Implement the SpaceAddRoomsScreen. (#4951)

* Update proxies ready for adding rooms to a space.

- ClientProxy.recentlyVisitedRooms to take a filter and to return room proxies.
- Expose SpaceServiceProxy.addChildToSpace.

* Implement the SpaceAddRoomsScreen.

* Fix the designs of the selected carousels.

Add more snapshot tests for inviting users.

* Fix for screen using wrong avatar size.
This commit is contained in:
Doug
2026-01-14 12:07:57 +00:00
committed by GitHub
parent 530e249eee
commit cde9714a83
61 changed files with 1060 additions and 102 deletions
@@ -37,8 +37,6 @@ struct InviteUsersScreenViewState: BindableState {
!isSearching && usersSection.type == .searchResult && usersSection.users.isEmpty
}
var scrollToLastID: String?
func isUserSelected(_ user: UserProfileProxy) -> Bool {
isUserDisabled(user) || selectedUsers.contains { $0.userID == user.userID }
}
@@ -69,6 +67,7 @@ struct InviteUsersScreenViewState: BindableState {
struct InviteUsersScreenViewStateBindings {
var searchQuery = ""
var selectedUsersPosition: String?
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<InviteUsersScreenErrorType>?
@@ -69,11 +69,10 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private func toggleUser(_ user: UserProfileProxy) {
if state.selectedUsers.contains(user) {
state.scrollToLastID = nil
state.selectedUsers.removeAll { $0.userID == user.userID }
} else {
state.scrollToLastID = user.userID
state.selectedUsers.append(user)
withElementAnimation(.easeInOut) { state.bindings.selectedUsersPosition = user.userID }
}
}
@@ -101,28 +101,22 @@ struct InviteUsersScreen: View {
}
}
@ScaledMetric private var cellWidth: CGFloat = 72
@ScaledMetric private var selectedUserCellWidth: CGFloat = 80
private var selectedUsersSection: some View {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { scrollView in
HStack(spacing: 16) {
ForEach(context.viewState.selectedUsers, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: context.mediaProvider) {
deselect(user)
}
.frame(width: cellWidth)
HStack(spacing: 8) {
ForEach(context.viewState.selectedUsers, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: context.mediaProvider) {
deselect(user)
}
.frame(width: selectedUserCellWidth)
}
.onChange(of: context.viewState.scrollToLastID) { _, lastAddedID in
guard let id = lastAddedID else { return }
withElementAnimation(.easeInOut) {
scrollView.scrollTo(id)
}
}
.padding(.horizontal, 14)
}
.padding(.horizontal, 16)
.scrollTargetLayout()
}
.scrollPosition(id: $context.selectedUsersPosition, anchor: .trailing)
}
@ToolbarContentBuilder
@@ -152,20 +146,54 @@ struct InviteUsersScreen: View {
// MARK: - Previews
struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = {
let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
return InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
roomProxy: JoinedRoomProxyMock(.init()),
isSkippable: true,
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let viewModel = makeViewModel()
static let searchingViewModel = makeViewModel(searchQuery: "Alice")
static let selectedViewModel = makeViewModel(hasSelection: true)
static var previews: some View {
NavigationStack {
InviteUsersScreen(context: viewModel.context)
}
.previewDisplayName("Suggestions")
.snapshotPreferences(expect: viewModel.context.$viewState.map { !$0.usersSection.users.isEmpty })
NavigationStack {
InviteUsersScreen(context: searchingViewModel.context)
}
.previewDisplayName("Searching")
.snapshotPreferences(expect: searchingViewModel.context.$viewState.map {
$0.usersSection.type == .searchResult && !$0.usersSection.users.isEmpty
})
NavigationStack {
InviteUsersScreen(context: selectedViewModel.context)
}
.previewDisplayName("Selected")
.snapshotPreferences(expect: selectedViewModel.context.$viewState.map { !$0.selectedUsers.isEmpty })
}
static func makeViewModel(searchQuery: String? = nil, hasSelection: Bool = false) -> InviteUsersScreenViewModel {
let clientProxy = ClientProxyMock(.init())
clientProxy.recentConversationCounterpartsReturnValue = [.mockAlice, .mockBob, .mockCharlie, .mockDan, .mockVerbose]
let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
roomProxy: JoinedRoomProxyMock(.init(members: [])),
isSkippable: true,
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
if let searchQuery {
viewModel.context.searchQuery = searchQuery
}
if hasSelection {
viewModel.state.selectedUsers = [.mockAlice]
}
return viewModel
}
}
@@ -6,6 +6,7 @@
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct InviteUsersScreenSelectedItem: View {
@@ -14,13 +15,13 @@ struct InviteUsersScreenSelectedItem: View {
let dismissAction: () -> Void
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 10) {
avatar
.accessibilityHidden(true)
Text(user.displayName ?? user.userID)
.font(.compound.bodyMD)
.foregroundColor(.compound.textPrimary)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
.accessibilityElement(children: .combine)
@@ -35,15 +36,14 @@ struct InviteUsersScreenSelectedItem: View {
contentID: user.userID,
avatarSize: .user(on: .inviteUsers),
mediaProvider: mediaProvider)
.overlay(alignment: .topTrailing) {
Button(action: dismissAction) {
Image(systemName: "xmark.circle.fill")
.resizable()
.scaledFrame(size: 20)
.symbolRenderingMode(.palette)
.foregroundStyle(Color.compound.iconOnSolidPrimary, Color.compound.iconPrimary)
}
}
.overlayRemoveItemButton(action: dismissAction)
}
var closeButtonLabel: some View {
CompoundIcon(\.close, size: .custom(12), relativeTo: .compound.bodySM)
.foregroundStyle(.compound.iconOnSolidPrimary)
.padding(2)
.background(.compound.iconPrimary, in: Circle())
}
}
@@ -52,10 +52,10 @@ struct InviteUsersScreenSelectedItem_Previews: PreviewProvider, TestablePreview
static var previews: some View {
ScrollView(.horizontal) {
HStack(spacing: 28) {
HStack(spacing: 8) {
ForEach(people, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: MediaProviderMock(configuration: .init())) { }
.frame(width: 72)
.frame(width: 80)
}
}
}
@@ -42,7 +42,7 @@ struct RoomChangeRolesScreenSelectedItem: View {
LoadableAvatarImage(url: member.avatarURL,
name: member.name,
contentID: member.id,
avatarSize: .user(on: .inviteUsers),
avatarSize: .user(on: .roomChangeRoles),
mediaProvider: mediaProvider)
.accessibilityHidden(true)
.overlay(alignment: .topTrailing) {
@@ -0,0 +1,55 @@
//
// Copyright 2026 Element Creations 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 SwiftUI
struct SpaceAddRoomsScreenCoordinatorParameters {
let spaceRoomListProxy: SpaceRoomListProxyProtocol
let userSession: UserSessionProtocol
let roomSummaryProvider: RoomSummaryProviderProtocol
let userIndicatorController: UserIndicatorControllerProtocol
}
enum SpaceAddRoomsScreenCoordinatorAction {
case dismiss
}
final class SpaceAddRoomsScreenCoordinator: CoordinatorProtocol {
private var viewModel: SpaceAddRoomsScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<SpaceAddRoomsScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<SpaceAddRoomsScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: SpaceAddRoomsScreenCoordinatorParameters) {
viewModel = SpaceAddRoomsScreenViewModel(spaceRoomListProxy: parameters.spaceRoomListProxy,
userSession: parameters.userSession,
roomSummaryProvider: parameters.roomSummaryProvider,
userIndicatorController: parameters.userIndicatorController)
}
func start() {
viewModel.actions.sink { [weak self] action in
switch action {
case .dismiss:
self?.actionsSubject.send(.dismiss)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(SpaceAddRoomsScreen(context: viewModel.context))
}
func stop() {
viewModel.stop()
}
}
@@ -0,0 +1,98 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
enum SpaceAddRoomsScreenViewModelAction {
case dismiss
}
struct SpaceAddRoomsScreenViewState: BindableState {
var roomsSection: Section
var selectedRooms: [SpaceAddRoomsScreenRoom] = []
var bindings = SpaceAddRoomsScreenViewStateBindings()
struct Section {
enum SectionType: Equatable { case searchResults, suggestions }
let type: SectionType
let rooms: [SpaceAddRoomsScreenRoom]
var title: String? {
switch type {
case .searchResults:
return nil
case .suggestions:
return rooms.isEmpty ? nil : L10n.commonSuggestions
}
}
}
}
struct SpaceAddRoomsScreenViewStateBindings {
var searchQuery = ""
var selectedRoomsPosition: String?
}
enum SpaceAddRoomsScreenViewAction {
case cancel
case reachedTop
case reachedBottom
case searchQueryChanged
case toggleRoom(SpaceAddRoomsScreenRoom)
case save
}
struct SpaceAddRoomsScreenRoom: Identifiable, Equatable {
let id: String
let title: String
let description: String
let avatar: RoomAvatar
}
extension SpaceAddRoomsScreenRoom {
init(summary: RoomSummary) {
self.init(id: summary.id,
title: summary.name,
description: summary.roomListDescription,
avatar: summary.avatar)
}
init(roomProxy: JoinedRoomProxyProtocol) {
self.init(id: roomProxy.id,
title: roomProxy.infoPublisher.value.displayName ?? roomProxy.id,
description: roomProxy.infoPublisher.value.roomListDescription,
avatar: roomProxy.infoPublisher.value.avatar)
}
}
private extension RoomInfoProxyProtocol {
var roomListDescription: String {
if isDirect {
return canonicalAlias ?? ""
}
if let alias = canonicalAlias {
return alias
}
guard heroes.count > 0 else {
return ""
}
var heroComponents = heroes.compactMap(\.displayName)
let othersCount = Int(activeMembersCount) - heroes.count
if othersCount > 0 {
heroComponents.append(L10n.commonManyMembers(othersCount))
}
return heroComponents.formatted(.list(type: .and))
}
}
@@ -0,0 +1,142 @@
//
// Copyright 2026 Element Creations 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 SwiftUI
typealias SpaceAddRoomsScreenViewModelType = StateStoreViewModelV2<SpaceAddRoomsScreenViewState, SpaceAddRoomsScreenViewAction>
class SpaceAddRoomsScreenViewModel: SpaceAddRoomsScreenViewModelType, SpaceAddRoomsScreenViewModelProtocol {
private let spaceRoomListProxy: SpaceRoomListProxyProtocol
private let spaceServiceProxy: SpaceServiceProxyProtocol
private let roomSummaryProvider: RoomSummaryProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private var suggestedRooms: [SpaceAddRoomsScreenRoom] = []
private var actionsSubject: PassthroughSubject<SpaceAddRoomsScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<SpaceAddRoomsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(spaceRoomListProxy: SpaceRoomListProxyProtocol,
userSession: UserSessionProtocol,
roomSummaryProvider: RoomSummaryProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.spaceRoomListProxy = spaceRoomListProxy
spaceServiceProxy = userSession.clientProxy.spaceService
self.roomSummaryProvider = roomSummaryProvider
self.userIndicatorController = userIndicatorController
super.init(initialViewState: SpaceAddRoomsScreenViewState(roomsSection: .init(type: .suggestions, rooms: [])),
mediaProvider: userSession.mediaProvider)
roomSummaryProvider.roomListPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateRooms()
}
.store(in: &cancellables)
Task {
let existingRooms = spaceRoomListProxy.spaceRoomsPublisher.value
suggestedRooms = await userSession.clientProxy
.recentlyVisitedRooms { roomProxy in
!roomProxy.infoPublisher.value.isDirect && !existingRooms.contains { $0.id == roomProxy.id }
}
.map { .init(roomProxy: $0) }
if state.roomsSection.type == .suggestions {
state.roomsSection = .init(type: .suggestions, rooms: suggestedRooms)
}
}
}
override func process(viewAction: SpaceAddRoomsScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.dismiss)
case .reachedTop:
updateVisibleRange(edge: .top)
case .reachedBottom:
updateVisibleRange(edge: .bottom)
case .searchQueryChanged where state.bindings.searchQuery.isEmpty:
roomSummaryProvider.setFilter(.all(filters: []))
case .searchQueryChanged:
roomSummaryProvider.setFilter(.search(query: state.bindings.searchQuery))
case .toggleRoom(let room):
toggleRoom(room)
case .save:
Task { await save() }
}
}
func stop() {
// This is a shared provider so we should reset the filtering when we are done with the view
roomSummaryProvider.setFilter(.all(filters: []))
}
// MARK: - Private
private func updateRooms() {
guard !state.bindings.searchQuery.isEmpty else {
state.roomsSection = .init(type: .suggestions, rooms: suggestedRooms)
return
}
let existingRooms = spaceRoomListProxy.spaceRoomsPublisher.value
var rooms = [SpaceAddRoomsScreenRoom]()
for summary in roomSummaryProvider.roomListPublisher.value where !summary.isDirect && !existingRooms.contains(where: { $0.id == summary.id }) {
rooms.append(.init(summary: summary))
}
state.roomsSection = .init(type: .searchResults, rooms: rooms)
// rooms crap
}
/// The actual range values don't matter as long as they contain the lower
/// or upper bounds. updateVisibleRange is a hybrid API that powers both
/// sliding sync visible range update and list paginations
/// For lists other than the home screen one we don't care about visible ranges,
/// we just need the respective bounds to be there to trigger a next page load or
/// a reset to just one page
private func updateVisibleRange(edge: UIRectEdge) {
switch edge {
case .top:
roomSummaryProvider.updateVisibleRange(0..<0)
case .bottom:
let roomCount = roomSummaryProvider.roomListPublisher.value.count
roomSummaryProvider.updateVisibleRange(roomCount..<roomCount)
default:
break
}
}
private func toggleRoom(_ room: SpaceAddRoomsScreenRoom) {
if state.selectedRooms.contains(room) {
state.selectedRooms.removeAll { $0.id == room.id }
} else {
state.selectedRooms.append(room)
withElementAnimation(.easeInOut) { state.bindings.selectedRoomsPosition = room.id }
}
}
private func save() async {
for room in state.selectedRooms {
if case .failure(let error) = await spaceServiceProxy.addChild(room.id, to: spaceRoomListProxy.id) {
MXLog.error("Failed adding room to space: \(error)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
updateRooms() // Hide any rooms that were already added.
return
}
}
actionsSubject.send(.dismiss)
}
}
@@ -0,0 +1,16 @@
//
// Copyright 2026 Element Creations 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
@MainActor
protocol SpaceAddRoomsScreenViewModelProtocol {
var actions: AnyPublisher<SpaceAddRoomsScreenViewModelAction, Never> { get }
var context: SpaceAddRoomsScreenViewModelType.Context { get }
func stop()
}
@@ -0,0 +1,211 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct SpaceAddRoomsScreen: View {
@Bindable var context: SpaceAddRoomsScreenViewModel.Context
@State private var formWidth = CGFloat.zero
var showTopSection: Bool {
!context.viewState.selectedRooms.isEmpty
}
var body: some View {
Form {
Section {
EmptyView()
} header: {
VStack(alignment: .leading, spacing: 24) {
Text(L10n.screenSpaceAddRoomsRoomAccessDescription)
.font(.compound.bodySM)
.foregroundStyle(.compound.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
if showTopSection {
selectedRoomsSection
.textCase(.none)
.frame(width: formWidth)
.padding(.bottom, -8)
}
}
.listRowInsets(EdgeInsets())
}
if !context.viewState.roomsSection.rooms.isEmpty {
roomsSection
}
}
.compoundList()
.navigationTitle(L10n.actionAddExistingRooms)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.searchController(query: $context.searchQuery, showsCancelButton: false)
.compoundSearchField()
.disableAutocorrection(true)
.onChange(of: context.searchQuery) { context.send(viewAction: .searchQueryChanged) }
.readWidth($formWidth)
}
@ScaledMetric private var selectedRoomCellWidth: CGFloat = 80
private var selectedRoomsSection: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(context.viewState.selectedRooms, id: \.id) { room in
SpaceAddRoomsScreenSelectedItem(room: room, mediaProvider: context.mediaProvider) {
context.send(viewAction: .toggleRoom(room))
}
.frame(width: selectedRoomCellWidth)
}
}
.padding(.horizontal, 16)
.scrollTargetLayout()
}
.scrollPosition(id: $context.selectedRoomsPosition, anchor: .trailing)
}
private var roomsSection: some View {
Section {
ForEach(context.viewState.roomsSection.rooms) { room in
SpaceAddRoomsListRow(room: room,
isSelected: context.viewState.selectedRooms.contains { $0.id == room.id },
context: context)
}
// Replace these with ScrollView's `scrollPosition` when dropping iOS 16.
} header: {
switch context.viewState.roomsSection.type {
case .searchResults:
emptyRectangle
.onAppear {
context.send(viewAction: .reachedTop)
}
case .suggestions:
if let sectionTitle = context.viewState.roomsSection.title {
Text(sectionTitle)
}
}
} footer: {
if context.viewState.roomsSection.type == .searchResults {
emptyRectangle
.onAppear {
context.send(viewAction: .reachedBottom)
}
}
}
}
/// The greedy size of Rectangle can create an issue with the navigation bar when the search is highlighted, so is best to use a fixed frame instead of hidden() or EmptyView()
private var emptyRectangle: some View {
Rectangle()
.frame(width: 0, height: 0)
.accessibilityHidden(true)
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
ToolbarButton(role: .cancel) {
context.send(viewAction: .cancel)
}
}
ToolbarItem(placement: .confirmationAction) {
ToolbarButton(role: .save) {
context.send(viewAction: .save)
}
.disabled(context.viewState.selectedRooms.isEmpty)
}
}
}
private struct SpaceAddRoomsListRow: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
let room: SpaceAddRoomsScreenRoom
let isSelected: Bool
let context: SpaceAddRoomsScreenViewModel.Context
var body: some View {
ListRow(label: .avatar(title: room.title,
description: room.description,
icon: avatar),
kind: .multiSelection(isSelected: isSelected) {
context.send(viewAction: .toggleRoom(room))
})
}
@ViewBuilder @MainActor
var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: room.avatar,
avatarSize: .room(on: .spaceAddRooms),
mediaProvider: context.mediaProvider)
.dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
.accessibilityHidden(true)
}
}
}
// MARK: - Previews
struct SpaceAddRoomsScreen_Previews: PreviewProvider, TestablePreview {
static let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
static let viewModel = makeViewModel()
static let searchingViewModel = makeViewModel(searchQuery: "Foundation")
static let selectedViewModel = makeViewModel(searchQuery: "Foundation", hasSelection: true)
static var previews: some View {
NavigationStack {
SpaceAddRoomsScreen(context: viewModel.context)
}
.previewDisplayName("Suggested")
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.roomsSection).map {
$0.type == .suggestions && !$0.rooms.isEmpty
})
NavigationStack {
SpaceAddRoomsScreen(context: searchingViewModel.context)
}
.previewDisplayName("Searching")
NavigationStack {
SpaceAddRoomsScreen(context: selectedViewModel.context)
}
.previewDisplayName("Selected")
}
static func makeViewModel(searchQuery: String? = nil, hasSelection: Bool = false) -> SpaceAddRoomsScreenViewModel {
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoomMock(.init(isSpace: true))))
let clientProxy = ClientProxyMock(.init())
clientProxy.recentlyVisitedRoomsFilterReturnValue = [
JoinedRoomProxyMock(.init(id: "1", name: "Room Name", canonicalAlias: "#room-name:example.com")),
JoinedRoomProxyMock(.init(id: "2", name: "Room Name", canonicalAlias: "#room-name:example.com")),
JoinedRoomProxyMock(.init(id: "3", name: "Room Name", canonicalAlias: "#room-name:example.com"))
]
let viewModel = SpaceAddRoomsScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
roomSummaryProvider: summaryProvider,
userIndicatorController: UserIndicatorControllerMock())
if let searchQuery {
viewModel.context.searchQuery = searchQuery
viewModel.context.send(viewAction: .searchQueryChanged)
}
if hasSelection {
viewModel.state.selectedRooms = Array(summaryProvider.roomListPublisher.value.prefix(2)).map(SpaceAddRoomsScreenRoom.init)
}
return viewModel
}
}
@@ -0,0 +1,52 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-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 Compound
import SwiftUI
struct SpaceAddRoomsScreenSelectedItem: View {
let room: SpaceAddRoomsScreenRoom
let mediaProvider: MediaProviderProtocol?
let dismissAction: () -> Void
var body: some View {
VStack(spacing: 10) {
avatar
.accessibilityHidden(true)
Text(room.title)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
.accessibilityElement(children: .combine)
.accessibilityAction(named: L10n.actionRemove, dismissAction)
}
// MARK: - Private
var avatar: some View {
RoomAvatarImage(avatar: room.avatar,
avatarSize: .room(on: .spaceAddRoomsSelected),
mediaProvider: mediaProvider)
.overlayRemoveItemButton(action: dismissAction)
}
}
struct SpaceAddRoomsScreenSelectedItem_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
SpaceAddRoomsScreenSelectedItem(room: .init(id: "",
title: "Selected Room",
description: "#selected:matrix.org",
avatar: .room(id: "",
name: "Selected Room",
avatarURL: .mockMXCAvatar)),
mediaProvider: MediaProviderMock(configuration: .init())) { }
.frame(width: 80)
}
}