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
@@ -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 }