mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Add SpaceRoomCell & Space…ProxyProtocols and use them on the SpaceListScreen. (#4404)
This commit is contained in:
@@ -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))
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user