mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Add the inviter to JoinRoomScreen when it's an invite. (#3093)
* Add inviter property to RoomProxyProtocol. * Extract the room cell inviter label into it's own view. * Use the new RoomInviterLabel in the JoinRoomScreen. * Fix bug with invite to a public room.
This commit is contained in:
@@ -595,6 +595,7 @@
|
||||
899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; };
|
||||
899793EFC63DF93C3E0141E7 /* RoomMemberDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */; };
|
||||
8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */; };
|
||||
8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */; };
|
||||
8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; };
|
||||
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; };
|
||||
8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */; };
|
||||
@@ -1667,6 +1668,7 @@
|
||||
7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
7E492690C8B27A892C194CC4 /* AdvancedSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInviterLabel.swift; sourceTree = "<group>"; };
|
||||
7EC2F1622C5BBABED6012E12 /* HeroImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroImage.swift; sourceTree = "<group>"; };
|
||||
7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -2792,6 +2794,7 @@
|
||||
648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */,
|
||||
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */,
|
||||
BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */,
|
||||
7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */,
|
||||
839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */,
|
||||
DE7C80EF77AD102053D3646E /* RoundedLabelItem.swift */,
|
||||
AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */,
|
||||
@@ -6445,6 +6448,7 @@
|
||||
42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */,
|
||||
D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */,
|
||||
04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */,
|
||||
8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */,
|
||||
F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */,
|
||||
4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */,
|
||||
BD0BE20DBCE31253AE4490A1 /* RoomListFiltersEmptyStateView.swift in Sources */,
|
||||
|
||||
@@ -8273,6 +8273,23 @@ class RoomProxyMock: RoomProxyProtocol {
|
||||
set(value) { underlyingMembership = value }
|
||||
}
|
||||
var underlyingMembership: Membership!
|
||||
var inviterCallsCount = 0
|
||||
var inviterCalled: Bool {
|
||||
return inviterCallsCount > 0
|
||||
}
|
||||
|
||||
var inviter: RoomMemberProxyProtocol? {
|
||||
get async {
|
||||
inviterCallsCount += 1
|
||||
if let inviterClosure = inviterClosure {
|
||||
return await inviterClosure()
|
||||
} else {
|
||||
return underlyingInviter
|
||||
}
|
||||
}
|
||||
}
|
||||
var underlyingInviter: RoomMemberProxyProtocol?
|
||||
var inviterClosure: (() async -> RoomMemberProxyProtocol?)?
|
||||
var hasOngoingCall: Bool {
|
||||
get { return underlyingHasOngoingCall }
|
||||
set(value) { underlyingHasOngoingCall = value }
|
||||
|
||||
@@ -97,6 +97,11 @@ extension RoomMemberProxyMock {
|
||||
membership: .join))
|
||||
}
|
||||
|
||||
static var mockNoName: RoomMemberProxyMock {
|
||||
RoomMemberProxyMock(with: .init(userID: "@anonymous:matrix.org",
|
||||
membership: .join))
|
||||
}
|
||||
|
||||
static var mockInvited: RoomMemberProxyMock {
|
||||
RoomMemberProxyMock(with: .init(userID: "@invited:matrix.org",
|
||||
displayName: "Invited",
|
||||
|
||||
@@ -35,6 +35,7 @@ struct RoomProxyMockConfiguration {
|
||||
|
||||
var members: [RoomMemberProxyMock] = .allMembers
|
||||
var ownUserID = RoomMemberProxyMock.mockMe.userID
|
||||
var inviter: RoomMemberProxyProtocol?
|
||||
|
||||
var canUserInvite = true
|
||||
var canUserTriggerRoomNotification = false
|
||||
@@ -85,6 +86,7 @@ extension RoomProxyMock {
|
||||
|
||||
ownUserID = configuration.ownUserID
|
||||
membership = .joined
|
||||
inviterClosure = { configuration.inviter }
|
||||
|
||||
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
|
||||
typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RoomInviterDetails: Equatable {
|
||||
let id: String
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
|
||||
let attributedInviteText: AttributedString
|
||||
|
||||
init(member: RoomMemberProxyProtocol) {
|
||||
id = member.userID
|
||||
displayName = member.displayName
|
||||
avatarURL = member.avatarURL
|
||||
|
||||
let nameOrLocalPart = if let displayName = member.displayName {
|
||||
displayName
|
||||
} else {
|
||||
String(member.userID.dropFirst().prefix { $0 != ":" })
|
||||
}
|
||||
|
||||
// Pre-compute the attributed string.
|
||||
let placeholder = "{displayname}"
|
||||
var string = AttributedString(L10n.screenInvitesInvitedYou(placeholder, id))
|
||||
var displayNameString = AttributedString(nameOrLocalPart)
|
||||
displayNameString.bold()
|
||||
displayNameString.foregroundColor = .compound.textPrimary
|
||||
string.replace(placeholder, with: displayNameString)
|
||||
attributedInviteText = string
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomInviterLabel: View {
|
||||
let inviter: RoomInviterDetails
|
||||
|
||||
let imageProvider: ImageProviderProtocol?
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
LoadableAvatarImage(url: inviter.avatarURL,
|
||||
name: inviter.displayName,
|
||||
contentID: inviter.id,
|
||||
avatarSize: .custom(16),
|
||||
imageProvider: imageProvider)
|
||||
.alignmentGuide(.firstTextBaseline) { $0[.bottom] * 0.8 }
|
||||
|
||||
Text(inviter.attributedInviteText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomInviterLabel_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 10) {
|
||||
RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockAlice),
|
||||
imageProvider: MockMediaProvider())
|
||||
RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockDan),
|
||||
imageProvider: MockMediaProvider())
|
||||
RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockNoName),
|
||||
imageProvider: MockMediaProvider())
|
||||
RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockCharlie),
|
||||
imageProvider: MockMediaProvider())
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
}
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
@@ -144,12 +144,6 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
case invite
|
||||
}
|
||||
|
||||
struct InviterDetails: Equatable {
|
||||
let userID: String
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
}
|
||||
|
||||
static let placeholderLastMessage = AttributedString("Hidden last message")
|
||||
|
||||
/// The list item identifier is it's room identifier.
|
||||
@@ -182,7 +176,7 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
|
||||
let avatar: RoomAvatar
|
||||
|
||||
let inviter: InviterDetails?
|
||||
let inviter: RoomInviterDetails?
|
||||
|
||||
let canonicalAlias: String?
|
||||
|
||||
@@ -215,12 +209,7 @@ extension HomeScreenRoom {
|
||||
let isCallShown = summary.hasOngoingCall
|
||||
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions))
|
||||
|
||||
var inviter: InviterDetails?
|
||||
if let roomMemberProxy = summary.inviter {
|
||||
inviter = .init(userID: roomMemberProxy.userID,
|
||||
displayName: roomMemberProxy.displayName,
|
||||
avatarURL: roomMemberProxy.avatarURL)
|
||||
}
|
||||
let inviter = summary.inviter.map(RoomInviterDetails.init)
|
||||
|
||||
self.init(id: identifier,
|
||||
roomID: summary.id,
|
||||
|
||||
@@ -74,17 +74,10 @@ struct HomeScreenInviteCell: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var inviterView: some View {
|
||||
if let invitedText = attributedInviteText, let name = room.inviter?.displayName {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
LoadableAvatarImage(url: room.inviter?.avatarURL,
|
||||
name: name,
|
||||
contentID: name,
|
||||
avatarSize: .custom(16),
|
||||
imageProvider: context.imageProvider)
|
||||
.alignmentGuide(.firstTextBaseline) { $0[.bottom] * 0.8 }
|
||||
|
||||
Text(invitedText)
|
||||
}
|
||||
if let inviter = room.inviter, !room.isDirect {
|
||||
RoomInviterLabel(inviter: inviter, imageProvider: context.imageProvider)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPlaceholder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,27 +123,7 @@ struct HomeScreenInviteCell: View {
|
||||
}
|
||||
|
||||
private var subtitle: String? {
|
||||
room.isDirect ? room.inviter?.userID : room.canonicalAlias
|
||||
}
|
||||
|
||||
private var attributedInviteText: AttributedString? {
|
||||
guard
|
||||
room.isDirect == false,
|
||||
let inviterName = room.inviter?.displayName,
|
||||
let inviterID = room.inviter?.userID
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let text = L10n.screenInvitesInvitedYou(inviterName, inviterID)
|
||||
var attributedString = AttributedString(text)
|
||||
attributedString.font = .compound.bodyMD
|
||||
attributedString.foregroundColor = .compound.textPlaceholder
|
||||
if let range = attributedString.range(of: inviterName) {
|
||||
attributedString[range].foregroundColor = .compound.textPrimary
|
||||
attributedString[range].font = .compound.bodyMDSemibold
|
||||
}
|
||||
return attributedString
|
||||
room.isDirect ? room.inviter?.id : room.canonicalAlias
|
||||
}
|
||||
|
||||
private var badge: some View {
|
||||
|
||||
@@ -35,6 +35,7 @@ struct JoinRoomScreenRoomDetails {
|
||||
let canonicalAlias: String?
|
||||
let avatar: RoomAvatar
|
||||
let memberCount: UInt
|
||||
let inviter: RoomInviterDetails?
|
||||
}
|
||||
|
||||
struct JoinRoomScreenViewState: BindableState {
|
||||
|
||||
@@ -81,7 +81,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
|
||||
defer {
|
||||
hideLoadingIndicator()
|
||||
updateRoomDetails()
|
||||
Task { await updateRoomDetails() }
|
||||
}
|
||||
|
||||
// Using only the preview API isn't enough as it's not capable
|
||||
@@ -91,13 +91,13 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
|
||||
if let roomProxy = await clientProxy.roomForIdentifier(roomID) {
|
||||
self.roomProxy = roomProxy
|
||||
updateRoomDetails()
|
||||
await updateRoomDetails()
|
||||
}
|
||||
|
||||
switch await clientProxy.roomPreviewForIdentifier(roomID, via: via) {
|
||||
case .success(let roomPreviewDetails):
|
||||
self.roomPreviewDetails = roomPreviewDetails
|
||||
updateRoomDetails()
|
||||
await updateRoomDetails()
|
||||
case .failure(.roomPreviewIsPrivate):
|
||||
break // Handled by the mode, we don't need an error indicator.
|
||||
case .failure:
|
||||
@@ -105,22 +105,24 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRoomDetails() {
|
||||
private func updateRoomDetails() async {
|
||||
let name = roomProxy?.name ?? roomPreviewDetails?.name
|
||||
let inviter = await roomProxy?.inviter.flatMap(RoomInviterDetails.init)
|
||||
state.roomDetails = JoinRoomScreenRoomDetails(name: name,
|
||||
topic: roomProxy?.topic ?? roomPreviewDetails?.topic,
|
||||
canonicalAlias: roomProxy?.canonicalAlias ?? roomPreviewDetails?.canonicalAlias,
|
||||
avatar: roomProxy?.avatar ?? .room(id: roomID, name: name ?? "", avatarURL: roomPreviewDetails?.avatarURL),
|
||||
memberCount: UInt(roomProxy?.activeMembersCount ?? Int(roomPreviewDetails?.memberCount ?? 0)))
|
||||
memberCount: UInt(roomProxy?.activeMembersCount ?? Int(roomPreviewDetails?.memberCount ?? 0)),
|
||||
inviter: inviter)
|
||||
|
||||
updateMode()
|
||||
}
|
||||
|
||||
private func updateMode() {
|
||||
if roomProxy?.isPublic ?? false || roomPreviewDetails?.isPublic ?? false {
|
||||
state.mode = .join
|
||||
} else if roomProxy?.membership == .invited || roomPreviewDetails?.isInvited ?? false {
|
||||
if roomProxy?.membership == .invited || roomPreviewDetails?.isInvited ?? false { // Check invites first to show Accept/Decline buttons on public rooms.
|
||||
state.mode = .invited
|
||||
} else if roomProxy?.isPublic ?? false || roomPreviewDetails?.isPublic ?? false {
|
||||
state.mode = .join
|
||||
} else if roomPreviewDetails?.canKnock ?? false, allowKnocking { // Knocking is not supported yet, the flag is purely for preview tests.
|
||||
state.mode = .knock
|
||||
} else {
|
||||
|
||||
@@ -62,6 +62,12 @@ struct JoinRoomScreen: View {
|
||||
BadgeLabel(title: "\(memberCount)", icon: \.userProfile, isHighlighted: false)
|
||||
}
|
||||
|
||||
if let inviter = context.viewState.roomDetails?.inviter {
|
||||
RoomInviterLabel(inviter: inviter, imageProvider: context.imageProvider)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
|
||||
if let topic = context.viewState.roomDetails?.topic {
|
||||
Text(topic)
|
||||
.font(.compound.bodyMD)
|
||||
|
||||
@@ -64,6 +64,12 @@ class RoomProxy: RoomProxyProtocol {
|
||||
room.membership()
|
||||
}
|
||||
|
||||
var inviter: RoomMemberProxyProtocol? {
|
||||
get async {
|
||||
await (try? roomListItem.roomInfo().inviter).map(RoomMemberProxy.init)
|
||||
}
|
||||
}
|
||||
|
||||
var isDirect: Bool {
|
||||
room.isDirect()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ protocol RoomProxyProtocol {
|
||||
var isFavourite: Bool { get async }
|
||||
var pinnedEventIDs: [String] { get async }
|
||||
var membership: Membership { get }
|
||||
var inviter: RoomMemberProxyProtocol? { get async }
|
||||
var hasOngoingCall: Bool { get }
|
||||
var canonicalAlias: String? { get }
|
||||
var ownUserID: String { get }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bfca845384d5d0a44aa1a3e322c7aa059c4286750d2e0ff2a47632096b95c926
|
||||
size 108298
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cd1441564319b3f2a6028ac242063014964d601913ea1f8a7ccd86d1d7d4ef4f
|
||||
size 121015
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0aa045fa03babdf4b7eb051cc1befb486524ef0a904fb5f55d8f41633dd06f20
|
||||
size 63819
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:56a4cb633080ff6f57162246d43e6dc2f8dbddd718c84181f348af1e1efeb47f
|
||||
size 79274
|
||||
Reference in New Issue
Block a user