Files
letro-ios/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift
2026-01-27 12:50:57 +02:00

279 lines
11 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2024-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import Compound
import SwiftUI
@MainActor
struct HomeScreenInviteCell: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
let room: HomeScreenRoom
let context: HomeScreenViewModel.Context
let hideInviteAvatars: Bool
private var avatar: RoomAvatar {
// DM invites avatars are broken, this is a workaround
// https://github.com/matrix-org/matrix-rust-sdk/issues/4825
if room.isDirect, let inviter = room.inviter {
.heroes([.init(userID: inviter.id, displayName: inviter.displayName, avatarURL: hideInviteAvatars ? nil : inviter.avatarURL)])
} else {
hideInviteAvatars ? room.avatar.removingAvatar : room.avatar
}
}
var body: some View {
HStack(alignment: .top, spacing: 16) {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: avatar,
avatarSize: .custom(52),
mediaProvider: context.mediaProvider)
.dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
.accessibilityHidden(true)
}
mainContent
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 16)
.padding(.trailing, 16)
.multilineTextAlignment(.leading)
.overlay(alignment: .bottom) {
separator
}
}
.padding(.top, 12)
.padding(.leading, 16)
.onTapGesture {
if let roomID = room.roomID {
context.send(viewAction: .selectRoom(roomIdentifier: roomID))
}
}
.accessibilityIdentifier(A11yIdentifiers.homeScreen.roomName(room.name))
}
// MARK: - Private
private var mainContent: some View {
VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .firstTextBaseline, spacing: 16) {
textualContent
badge
}
inviterView
.padding(.top, 6)
.padding(.trailing, 16)
}
.fixedSize(horizontal: false, vertical: true)
.accessibilityElement(children: .combine)
buttons
.padding(.top, 14)
.padding(.trailing, 22)
}
}
@ViewBuilder
private var inviterView: some View {
if let inviter = room.inviter,
!room.isDirect {
RoomInviterLabel(inviter: inviter,
shouldHideAvatar: hideInviteAvatars,
mediaProvider: context.mediaProvider)
.font(.compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
}
}
private var textualContent: some View {
VStack(alignment: .leading, spacing: 0) {
Text(title)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
.lineLimit(2)
if let subtitle {
Text(subtitle)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var buttons: some View {
HStack(spacing: 12) {
Button {
context.send(viewAction: .declineInvite(roomIdentifier: room.id))
} label: {
Text(L10n.actionDecline)
.frame(maxWidth: .infinity)
}
.buttonStyle(.compound(.secondary, size: .medium))
Button {
context.send(viewAction: .acceptInvite(roomIdentifier: room.id))
} label: {
Text(L10n.actionAccept)
.frame(maxWidth: .infinity)
}
.buttonStyle(.compound(.primary, size: .medium))
}
}
private var separator: some View {
Rectangle()
.fill(Color.compound.borderDisabled)
.frame(height: 1 / UIScreen.main.scale)
}
private var title: String {
room.name
}
private var subtitle: String? {
room.isDirect ? room.inviter?.id : nil
}
@ViewBuilder
private var badge: some View {
if room.badges.isDotShown {
Circle()
.scaledFrame(size: 12)
.foregroundColor(.compound.iconAccentTertiary) // The badge is always green, no need to check isHighlighted here.
}
}
}
// MARK: - Previews
import MatrixRustSDKMocks
struct HomeScreenInviteCell_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 0) {
HomeScreenInviteCell(room: .dmInvite,
context: makeViewModel().context,
hideInviteAvatars: false)
HomeScreenInviteCell(room: .invite(),
context: makeViewModel().context,
hideInviteAvatars: false)
HomeScreenInviteCell(room: .invite(alias: "#footest:somewhere.org",
avatarURL: .mockMXCAvatar),
context: makeViewModel().context,
hideInviteAvatars: false)
// Not the final design, may get its own cell type entirely.
HomeScreenInviteCell(room: .invite(name: "Awesome Space",
isSpace: true,
alias: "#footest:somewhere.org",
avatarURL: .mockMXCAvatar),
context: makeViewModel().context,
hideInviteAvatars: false)
HomeScreenInviteCell(room: .invite(name: "Hidden Avatars",
avatarURL: .mockMXCAvatar),
context: makeViewModel().context,
hideInviteAvatars: true)
HomeScreenInviteCell(room: .invite(alias: "#footest:somewhere.org"),
context: makeViewModel().context,
hideInviteAvatars: false)
.dynamicTypeSize(.accessibility1)
.previewDisplayName("Aliased room (AX1)")
}
.previewLayout(.sizeThatFits)
}
static func makeViewModel() -> HomeScreenViewModel {
let clientProxy = ClientProxyMock(.init())
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
return HomeScreenViewModel(userSession: userSession,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}
@MainActor
private extension HomeScreenRoom {
static var dmInvite: HomeScreenRoom {
let inviter = RoomMemberProxyMock()
inviter.displayName = "Some Guy"
inviter.userID = "@someone:somewhere.com"
let summary = RoomSummary(room: RoomSDKMock(),
id: "@someone:somewhere.com",
joinRequestType: .invite(inviter: inviter),
name: "Some Guy",
isDirect: true,
isSpace: false,
avatarURL: nil,
heroes: [.init(userID: "@someone:somewhere.com")],
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: "#footest:somewhere.org",
alternativeAliases: [],
hasOngoingCall: false,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false)
return .init(summary: summary, hideUnreadMessagesBadge: false)
}
static func invite(name: String = "Awesome Room",
isSpace: Bool = false,
alias: String? = nil,
avatarURL: URL? = nil) -> HomeScreenRoom {
let inviter = RoomMemberProxyMock()
inviter.displayName = "Luca"
inviter.userID = "@jack:somewhi.nl"
inviter.avatarURL = avatarURL.map { _ in .mockMXCUserAvatar }
let summary = RoomSummary(room: RoomSDKMock(),
id: "@someone:somewhere.com",
joinRequestType: .invite(inviter: inviter),
name: name,
isDirect: false,
isSpace: isSpace,
avatarURL: avatarURL,
heroes: [.init(userID: "@someone:somewhere.com")],
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: alias,
alternativeAliases: [],
hasOngoingCall: false,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false)
return .init(summary: summary, hideUnreadMessagesBadge: false)
}
}