Files
letro-ios/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift
Doug e6efdf0e82 Include the Members and Add Rooms screens in the spaces UI tests. (#4981)
* Expand the space flow UI tests to include the add rooms screen.

Also fixes some failures in these tests due to the room/space title now being a button.

* Expand the space flow UI tests to include the space members screen.

* Reset AppSettings before running preview tests.
2026-01-21 15:24:54 +00:00

199 lines
8.9 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// 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 Compound
import SwiftUI
struct SpaceScreen: View {
@Bindable var context: SpaceScreenViewModel.Context
private var isEditModeActive: Bool { context.viewState.editMode != .inactive }
var body: some View {
ScrollView {
LazyVStack(spacing: 0) {
if !isEditModeActive {
SpaceHeaderView(spaceServiceRoom: context.viewState.space,
mediaProvider: context.mediaProvider)
}
rooms
}
}
.environment(\.editMode, .constant(context.viewState.editMode))
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.toolbarRole(RoomHeaderView.toolbarRole)
.navigationTitle(context.viewState.space.name)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(isEditModeActive)
.toolbar { toolbar }
.sheet(isPresented: $context.isPresentingRemoveChildrenConfirmation) {
SpaceRemoveChildrenConfirmationView(spaceName: context.viewState.space.name) {
context.send(viewAction: .confirmRemoveSelectedChildren)
}
}
.sheet(item: $context.leaveSpaceViewModel) { leaveSpaceViewModel in
LeaveSpaceView(context: leaveSpaceViewModel.context)
}
}
@ViewBuilder
var rooms: some View {
ForEach(context.viewState.visibleRooms, id: \.id) { spaceServiceRoom in
SpaceRoomCell(spaceServiceRoom: spaceServiceRoom,
isSelected: context.viewState.isSpaceIDSelected(spaceServiceRoom.id),
isJoining: context.viewState.joiningRoomIDs.contains(spaceServiceRoom.id),
mediaProvider: context.mediaProvider) { action in
context.send(viewAction: .spaceAction(action))
}
}
if context.viewState.isPaginating {
ProgressView()
.padding()
}
}
@ToolbarContentBuilder
var toolbar: some ToolbarContent {
if isEditModeActive {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel, role: .cancel) {
context.send(viewAction: .finishManagingChildren)
}
}
}
// Use the same trick as the RoomScreen for a leading title view that
// also hides the navigation title.
ToolbarItem(placement: .principal) {
RoomHeaderView(roomName: context.viewState.space.name,
roomAvatar: context.viewState.space.avatar,
mediaProvider: context.mediaProvider) {
if context.viewState.isSpaceManagementEnabled,
let roomProxy = context.viewState.roomProxy {
context.send(viewAction: .spaceSettings(roomProxy: roomProxy))
}
}
}
if isEditModeActive {
ToolbarItem(placement: .primaryAction) {
ToolbarButton(role: .destructive(title: L10n.actionRemove)) {
context.send(viewAction: .removeSelectedChildren)
}
.disabled(context.viewState.editModeSelectedIDs.isEmpty)
}
} else {
// This should really use a ToolbarItemGroup(placement: .secondaryAction), however it
// was crashing on iOS 26.0 when tapping the ShareLink as the popover presentation
// controller attempts to anchor itself to the button that is no longer visible.
ToolbarItem(placement: .primaryAction) {
Menu {
if true {
Section {
Button { context.send(viewAction: .addExistingRooms) } label: {
Label(L10n.actionAddExistingRooms, icon: \.room)
}
.accessibilityIdentifier(A11yIdentifiers.spaceScreen.addExistingRooms)
Button { context.send(viewAction: .manageChildren) } label: {
Label(L10n.actionManageRooms, icon: \.edit)
}
}
}
Section {
if let roomProxy = context.viewState.roomProxy {
Button { context.send(viewAction: .displayMembers(roomProxy: roomProxy)) } label: {
Label(L10n.screenSpaceMenuActionMembers, icon: \.user)
}
.accessibilityIdentifier(A11yIdentifiers.spaceScreen.viewMembers)
}
if let permalink = context.viewState.permalink {
ShareLink(item: permalink) {
Label(L10n.actionShare, icon: \.shareIos)
}
}
if context.viewState.isSpaceManagementEnabled,
let roomProxy = context.viewState.roomProxy {
Button { context.send(viewAction: .spaceSettings(roomProxy: roomProxy)) } label: {
Label(L10n.commonSettings, icon: \.settings)
}
}
}
Section {
Button(role: .destructive) { context.send(viewAction: .leaveSpace) } label: {
Label(L10n.actionLeaveSpace, icon: \.leave)
}
}
} label: {
// Use an SF Symbol to match what ToolbarItemGroup(placement: .secondaryAction) would give us.
Image(systemSymbol: .ellipsis)
}
.accessibilityIdentifier(A11yIdentifiers.spaceScreen.moreMenu)
}
}
}
}
// MARK: - Previews
struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = makeViewModel()
static let managingViewModel = makeViewModel(isManagingRooms: true)
static var previews: some View {
NavigationStack {
SpaceScreen(context: viewModel.context)
}
NavigationStack {
SpaceScreen(context: managingViewModel.context)
}
.previewDisplayName("Managing")
}
static func makeViewModel(isManagingRooms: Bool = false) -> SpaceScreenViewModel {
let spaceServiceRoom = SpaceServiceRoomMock(.init(id: "!eng-space:matrix.org",
name: "Engineering Team",
isSpace: true,
childrenCount: 30,
joinedMembersCount: 76,
heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose],
topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.",
canonicalAlias: "#engineering-team:element.io",
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")])))
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: spaceServiceRoom,
initialSpaceRooms: .mockSpaceList))
let clientProxy = ClientProxyMock(.init())
clientProxy.roomForIdentifierClosure = { _ in
.joined(JoinedRoomProxyMock(.init()))
}
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
spaceServiceProxy: SpaceServiceProxyMock(.init()),
selectedSpaceRoomPublisher: .init(nil),
userSession: userSession,
appSettings: AppSettings(),
userIndicatorController: UserIndicatorControllerMock())
if isManagingRooms {
viewModel.state.editMode = .transient
viewModel.state.editModeSelectedIDs = [viewModel.state.visibleRooms[0].id]
}
return viewModel
}
}