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 */; };
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