Add SpaceRoomCell & Space…ProxyProtocols and use them on the SpaceListScreen. (#4404)

This commit is contained in:
Doug
2025-08-13 12:06:29 +01:00
committed by GitHub
parent 192f4765c3
commit 3dc6d73899
32 changed files with 781 additions and 39 deletions
@@ -0,0 +1,179 @@
//
// Copyright 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
struct SpaceRoomCell: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
let spaceRoom: SpaceRoomProxyProtocol
let isSelected: Bool
let mediaProvider: MediaProviderProtocol!
enum Action { case select(SpaceRoomProxyProtocol), join(SpaceRoomProxyProtocol) }
let action: (Action) -> Void
private let verticalInsets = 12.0
private let horizontalInsets = 16.0
private var subtitle: String {
if spaceRoom.isSpace {
spaceRoom.joinRule == .public ? L10n.commonPublicSpace : L10n.commonPrivateSpace
} else {
L10n.commonMemberCount(spaceRoom.joinedMembersCount)
}
}
private var details: String {
if spaceRoom.isSpace {
L10n.screenSpaceListDetails(L10n.commonRooms(spaceRoom.childrenCount),
L10n.commonMemberCount(spaceRoom.joinedMembersCount))
} else {
spaceRoom.topic ?? " " // Use a single space to reserve a consistent amount of space.
}
}
var body: some View {
Button {
action(.select(spaceRoom))
} label: {
HStack(spacing: 16.0) {
avatar
content
.padding(.vertical, verticalInsets)
.overlay(alignment: .bottom) {
Rectangle()
.fill(Color.compound.borderDisabled)
.frame(height: 1 / UIScreen.main.scale)
.padding(.trailing, -horizontalInsets)
}
}
.padding(.horizontal, horizontalInsets)
.accessibilityElement(children: .combine)
}
.buttonStyle(SpaceRoomCellButtonStyle(isSelected: isSelected))
.accessibilityIdentifier(A11yIdentifiers.spaceListScreen.spaceRoomName(spaceRoom.name ?? spaceRoom.id))
}
@ViewBuilder @MainActor
private var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: spaceRoom.avatar,
avatarSize: .room(on: .spaces),
mediaProvider: mediaProvider)
.dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
.accessibilityHidden(true)
}
}
private var content: some View {
HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 2) {
Text(spaceRoom.name ?? spaceRoom.id)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
.lineLimit(1)
visibilityLabel
Text(details)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
accessory
}
}
private var visibilityLabel: some View {
Label {
Text(subtitle)
.font(.compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
.lineLimit(1)
} icon: {
CompoundIcon(spaceRoom.joinRule == .public ? \.public : \.lockSolid,
size: .xSmall,
relativeTo: .compound.bodyMD)
.foregroundStyle(.compound.iconTertiary)
}
.labelStyle(.custom(spacing: 4))
}
@ViewBuilder
private var accessory: some View {
switch spaceRoom.state {
case .none, .left, .invited:
Button(L10n.actionJoin) { action(.join(spaceRoom)) }
.font(.compound.bodyLG)
.foregroundStyle(.compound.textActionAccent)
case .joined, .knocked, .banned:
EmptyView()
}
}
}
struct SpaceRoomCellButtonStyle: ButtonStyle {
let isSelected: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(isSelected || configuration.isPressed ? Color.compound.bgSubtleSecondary : Color.compound.bgCanvasDefault)
.contentShape(Rectangle())
.animation(isSelected ? .none : .easeOut(duration: 0.1).disabledDuringTests(), value: isSelected)
}
}
struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview {
static let mediaProvider = MediaProviderMock(configuration: .init())
static let spaces = makeSpaceRooms(isSpace: true)
static let rooms = makeSpaceRooms(isSpace: false)
static var previews: some View {
VStack(spacing: 0) {
ForEach(spaces, id: \.id) { space in
SpaceRoomCell(spaceRoom: space,
isSelected: false,
mediaProvider: mediaProvider) { _ in }
}
ForEach(rooms, id: \.id) { room in
SpaceRoomCell(spaceRoom: room,
isSelected: false,
mediaProvider: mediaProvider) { _ in }
}
}
}
static func makeSpaceRooms(isSpace: Bool) -> [SpaceRoomProxyMock] {
let name = isSpace ? "Space" : "Room"
return [
SpaceRoomProxyMock(.init(id: "!space1:matrix.org",
name: "Company \(name)",
isSpace: isSpace)),
SpaceRoomProxyMock(.init(id: "!space2:matrix.org",
name: "Public \(name)",
avatarURL: .mockMXCAvatar,
isSpace: isSpace,
joinedMembersCount: 78,
topic: "Discussion on specific topic goes here.",
joinRule: .public)),
SpaceRoomProxyMock(.init(id: "!space3:matrix.org",
name: "Joined \(name)",
isSpace: isSpace,
joinedMembersCount: 123,
topic: "Discussion on specific topic goes here.",
state: .joined))
]
}
}