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:
Doug
2024-07-29 09:57:49 +01:00
committed by GitHub
parent 9518e8a219
commit 0fab3a8afc
16 changed files with 156 additions and 53 deletions
+4
View File
@@ -595,6 +595,7 @@
899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; 899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; };
899793EFC63DF93C3E0141E7 /* RoomMemberDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */; }; 899793EFC63DF93C3E0141E7 /* RoomMemberDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */; };
8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.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 */; }; 8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; };
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; }; 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; };
8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = "<group>"; };
@@ -2792,6 +2794,7 @@
648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */, 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */,
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */, C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */,
BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */, BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */,
7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */,
839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */, 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */,
DE7C80EF77AD102053D3646E /* RoundedLabelItem.swift */, DE7C80EF77AD102053D3646E /* RoundedLabelItem.swift */,
AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */, AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */,
@@ -6445,6 +6448,7 @@
42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */, 42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */,
D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */, D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */,
04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */, 04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */,
8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */,
F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */, F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */,
4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */, 4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */,
BD0BE20DBCE31253AE4490A1 /* RoomListFiltersEmptyStateView.swift in Sources */, BD0BE20DBCE31253AE4490A1 /* RoomListFiltersEmptyStateView.swift in Sources */,
@@ -8273,6 +8273,23 @@ class RoomProxyMock: RoomProxyProtocol {
set(value) { underlyingMembership = value } set(value) { underlyingMembership = value }
} }
var underlyingMembership: Membership! 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 { var hasOngoingCall: Bool {
get { return underlyingHasOngoingCall } get { return underlyingHasOngoingCall }
set(value) { underlyingHasOngoingCall = value } set(value) { underlyingHasOngoingCall = value }
@@ -97,6 +97,11 @@ extension RoomMemberProxyMock {
membership: .join)) membership: .join))
} }
static var mockNoName: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@anonymous:matrix.org",
membership: .join))
}
static var mockInvited: RoomMemberProxyMock { static var mockInvited: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@invited:matrix.org", RoomMemberProxyMock(with: .init(userID: "@invited:matrix.org",
displayName: "Invited", displayName: "Invited",
@@ -35,6 +35,7 @@ struct RoomProxyMockConfiguration {
var members: [RoomMemberProxyMock] = .allMembers var members: [RoomMemberProxyMock] = .allMembers
var ownUserID = RoomMemberProxyMock.mockMe.userID var ownUserID = RoomMemberProxyMock.mockMe.userID
var inviter: RoomMemberProxyProtocol?
var canUserInvite = true var canUserInvite = true
var canUserTriggerRoomNotification = false var canUserTriggerRoomNotification = false
@@ -85,6 +86,7 @@ extension RoomProxyMock {
ownUserID = configuration.ownUserID ownUserID = configuration.ownUserID
membership = .joined membership = .joined
inviterClosure = { configuration.inviter }
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher() membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
typingMembersPublisher = CurrentValueSubject([]).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 case invite
} }
struct InviterDetails: Equatable {
let userID: String
let displayName: String?
let avatarURL: URL?
}
static let placeholderLastMessage = AttributedString("Hidden last message") static let placeholderLastMessage = AttributedString("Hidden last message")
/// The list item identifier is it's room identifier. /// The list item identifier is it's room identifier.
@@ -182,7 +176,7 @@ struct HomeScreenRoom: Identifiable, Equatable {
let avatar: RoomAvatar let avatar: RoomAvatar
let inviter: InviterDetails? let inviter: RoomInviterDetails?
let canonicalAlias: String? let canonicalAlias: String?
@@ -215,12 +209,7 @@ extension HomeScreenRoom {
let isCallShown = summary.hasOngoingCall let isCallShown = summary.hasOngoingCall
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions))
var inviter: InviterDetails? let inviter = summary.inviter.map(RoomInviterDetails.init)
if let roomMemberProxy = summary.inviter {
inviter = .init(userID: roomMemberProxy.userID,
displayName: roomMemberProxy.displayName,
avatarURL: roomMemberProxy.avatarURL)
}
self.init(id: identifier, self.init(id: identifier,
roomID: summary.id, roomID: summary.id,
@@ -74,17 +74,10 @@ struct HomeScreenInviteCell: View {
@ViewBuilder @ViewBuilder
private var inviterView: some View { private var inviterView: some View {
if let invitedText = attributedInviteText, let name = room.inviter?.displayName { if let inviter = room.inviter, !room.isDirect {
HStack(alignment: .firstTextBaseline, spacing: 8) { RoomInviterLabel(inviter: inviter, imageProvider: context.imageProvider)
LoadableAvatarImage(url: room.inviter?.avatarURL, .font(.compound.bodyMD)
name: name, .foregroundStyle(.compound.textPlaceholder)
contentID: name,
avatarSize: .custom(16),
imageProvider: context.imageProvider)
.alignmentGuide(.firstTextBaseline) { $0[.bottom] * 0.8 }
Text(invitedText)
}
} }
} }
@@ -130,27 +123,7 @@ struct HomeScreenInviteCell: View {
} }
private var subtitle: String? { private var subtitle: String? {
room.isDirect ? room.inviter?.userID : room.canonicalAlias room.isDirect ? room.inviter?.id : 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
} }
private var badge: some View { private var badge: some View {
@@ -35,6 +35,7 @@ struct JoinRoomScreenRoomDetails {
let canonicalAlias: String? let canonicalAlias: String?
let avatar: RoomAvatar let avatar: RoomAvatar
let memberCount: UInt let memberCount: UInt
let inviter: RoomInviterDetails?
} }
struct JoinRoomScreenViewState: BindableState { struct JoinRoomScreenViewState: BindableState {
@@ -81,7 +81,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
defer { defer {
hideLoadingIndicator() hideLoadingIndicator()
updateRoomDetails() Task { await updateRoomDetails() }
} }
// Using only the preview API isn't enough as it's not capable // 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) { if let roomProxy = await clientProxy.roomForIdentifier(roomID) {
self.roomProxy = roomProxy self.roomProxy = roomProxy
updateRoomDetails() await updateRoomDetails()
} }
switch await clientProxy.roomPreviewForIdentifier(roomID, via: via) { switch await clientProxy.roomPreviewForIdentifier(roomID, via: via) {
case .success(let roomPreviewDetails): case .success(let roomPreviewDetails):
self.roomPreviewDetails = roomPreviewDetails self.roomPreviewDetails = roomPreviewDetails
updateRoomDetails() await updateRoomDetails()
case .failure(.roomPreviewIsPrivate): case .failure(.roomPreviewIsPrivate):
break // Handled by the mode, we don't need an error indicator. break // Handled by the mode, we don't need an error indicator.
case .failure: case .failure:
@@ -105,22 +105,24 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
} }
} }
private func updateRoomDetails() { private func updateRoomDetails() async {
let name = roomProxy?.name ?? roomPreviewDetails?.name let name = roomProxy?.name ?? roomPreviewDetails?.name
let inviter = await roomProxy?.inviter.flatMap(RoomInviterDetails.init)
state.roomDetails = JoinRoomScreenRoomDetails(name: name, state.roomDetails = JoinRoomScreenRoomDetails(name: name,
topic: roomProxy?.topic ?? roomPreviewDetails?.topic, topic: roomProxy?.topic ?? roomPreviewDetails?.topic,
canonicalAlias: roomProxy?.canonicalAlias ?? roomPreviewDetails?.canonicalAlias, canonicalAlias: roomProxy?.canonicalAlias ?? roomPreviewDetails?.canonicalAlias,
avatar: roomProxy?.avatar ?? .room(id: roomID, name: name ?? "", avatarURL: roomPreviewDetails?.avatarURL), 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() updateMode()
} }
private func updateMode() { private func updateMode() {
if roomProxy?.isPublic ?? false || roomPreviewDetails?.isPublic ?? false { if roomProxy?.membership == .invited || roomPreviewDetails?.isInvited ?? false { // Check invites first to show Accept/Decline buttons on public rooms.
state.mode = .join
} else if roomProxy?.membership == .invited || roomPreviewDetails?.isInvited ?? false {
state.mode = .invited 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. } else if roomPreviewDetails?.canKnock ?? false, allowKnocking { // Knocking is not supported yet, the flag is purely for preview tests.
state.mode = .knock state.mode = .knock
} else { } else {
@@ -62,6 +62,12 @@ struct JoinRoomScreen: View {
BadgeLabel(title: "\(memberCount)", icon: \.userProfile, isHighlighted: false) 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 { if let topic = context.viewState.roomDetails?.topic {
Text(topic) Text(topic)
.font(.compound.bodyMD) .font(.compound.bodyMD)
@@ -64,6 +64,12 @@ class RoomProxy: RoomProxyProtocol {
room.membership() room.membership()
} }
var inviter: RoomMemberProxyProtocol? {
get async {
await (try? roomListItem.roomInfo().inviter).map(RoomMemberProxy.init)
}
}
var isDirect: Bool { var isDirect: Bool {
room.isDirect() room.isDirect()
} }
@@ -40,6 +40,7 @@ protocol RoomProxyProtocol {
var isFavourite: Bool { get async } var isFavourite: Bool { get async }
var pinnedEventIDs: [String] { get async } var pinnedEventIDs: [String] { get async }
var membership: Membership { get } var membership: Membership { get }
var inviter: RoomMemberProxyProtocol? { get async }
var hasOngoingCall: Bool { get } var hasOngoingCall: Bool { get }
var canonicalAlias: String? { get } var canonicalAlias: String? { get }
var ownUserID: 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