diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 0b6f85e68..24b282458 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -1096,6 +1096,7 @@ C8BD80891BAD688EF2C15CDB /* MediaUploadPreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */; }; C8D1D18E22672D48C11A5366 /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */; }; C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; }; + C8E11A335456FCF94A744E6E /* SpaceFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */; }; C8E1E4E06B7C7A3A8246FC9B /* MediaEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */; }; C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; C969A62F3D9F14318481A33B /* KnockedRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */; }; @@ -2690,6 +2691,7 @@ ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; + EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = ""; }; EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -4193,6 +4195,7 @@ 0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */, D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */, 5A4EF5724C0F894911AF7811 /* SpaceExplorerFlowCoordinator.swift */, + EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */, C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */, ); path = FlowCoordinators; @@ -8142,6 +8145,7 @@ F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */, CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */, 8D9A97E32C6C03B884CBD85A /* SpaceExplorerFlowCoordinator.swift in Sources */, + C8E11A335456FCF94A744E6E /* SpaceFlowCoordinator.swift in Sources */, E9B4742B3D6E103327466513 /* SpaceHeaderView.swift in Sources */, 306ADA9D91EE5F0A30B5E500 /* SpaceListScreen.swift in Sources */, C586E1B286BCD8A774DA16B8 /* SpaceListScreenCoordinator.swift in Sources */, diff --git a/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift index 820eaec7e..4fc9c30b4 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift @@ -22,21 +22,31 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol { private let userIndicatorController: UserIndicatorControllerProtocol + private var spaceFlowCoordinator: SpaceFlowCoordinator? + enum State: StateType { /// The state machine hasn't started. case initial /// The root screen for this flow. - case spaceList + case spaceList(selectedSpaceID: String?) } enum Event: EventType { /// The flow is being started. case start + /// Request presentation for a particular space. + /// + /// The space's `SpaceRoomListProxyProtocol` must be provided in the `userInfo`. + case selectSpace + /// The space screen has been dismissed. + case deselectSpace } private let stateMachine: StateMachine private var cancellables: Set = [] + private let selectedSpaceSubject = CurrentValueSubject(nil) + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -77,30 +87,79 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol { // MARK: - Private private func configureStateMachine() { - stateMachine.addRoutes(event: .start, transitions: [.initial => .spaceList]) { [weak self] _ in + stateMachine.addRoutes(event: .start, transitions: [.initial => .spaceList(selectedSpaceID: nil)]) { [weak self] _ in self?.presentSpaceList() } + stateMachine.addRouteMapping { event, fromState, userInfo in + guard event == .selectSpace, case .spaceList = fromState else { return nil } + guard let spaceRoomListProxy = userInfo as? SpaceRoomListProxyProtocol else { fatalError("A space proxy must be provided.") } + return .spaceList(selectedSpaceID: spaceRoomListProxy.spaceRoom.id) + } handler: { [weak self] context in + guard let self, let spaceRoomListProxy = context.userInfo as? SpaceRoomListProxyProtocol else { return } + startSpaceFlow(spaceRoomListProxy: spaceRoomListProxy) + } + + stateMachine.addRouteMapping { event, fromState, _ in + guard event == .deselectSpace, case .spaceList(.some) = fromState else { return nil } + return .spaceList(selectedSpaceID: nil) + } handler: { [weak self] _ in + guard let self else { return } + selectedSpaceSubject.send(nil) + spaceFlowCoordinator = nil + } + stateMachine.addErrorHandler { context in fatalError("Unexpected transition: \(context)") } } private func presentSpaceList() { - // Temporarily using the mock until the SDK is updated. - let parameters = SpaceListScreenCoordinatorParameters(userSession: userSession, spaceServiceProxy: SpaceServiceProxyMock(.init())) + let parameters = SpaceListScreenCoordinatorParameters(userSession: userSession, + selectedSpaceSubject: selectedSpaceSubject.asCurrentValuePublisher(), + userIndicatorController: userIndicatorController) let coordinator = SpaceListScreenCoordinator(parameters: parameters) - coordinator.actionsPublisher.sink { [weak self] action in - guard let self else { return } - switch action { - case .showSettings: - actionsSubject.send(.showSettings) - case .selectSpace: - break + coordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + switch action { + case .selectSpace(let spaceRoomListProxy): + stateMachine.tryEvent(.selectSpace, userInfo: spaceRoomListProxy) + case .showSettings: + actionsSubject.send(.showSettings) + } } - } - .store(in: &cancellables) + .store(in: &cancellables) sidebarNavigationStackCoordinator.setRootCoordinator(coordinator) } + + private func startSpaceFlow(spaceRoomListProxy: SpaceRoomListProxyProtocol) { + let coordinator = SpaceFlowCoordinator(spaceRoomListProxy: spaceRoomListProxy, + spaceServiceProxy: userSession.clientProxy.spaceService, + isChildFlow: false, + mediaProvider: userSession.mediaProvider, + navigationStackCoordinator: detailNavigationStackCoordinator, + userIndicatorController: userIndicatorController) + + coordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .finished: + stateMachine.tryEvent(.deselectSpace) + } + } + .store(in: &cancellables) + + spaceFlowCoordinator = coordinator + + if navigationSplitCoordinator.detailCoordinator !== detailNavigationStackCoordinator { + navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator) + } + + coordinator.start() + selectedSpaceSubject.send(spaceRoomListProxy.spaceRoom.id) + } } diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift new file mode 100644 index 000000000..ac250961b --- /dev/null +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -0,0 +1,181 @@ +// +// 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 Foundation +import SwiftState + +enum SpaceFlowCoordinatorAction: Equatable { + case finished +} + +class SpaceFlowCoordinator: FlowCoordinatorProtocol { + private let spaceRoomListProxy: SpaceRoomListProxyProtocol + private let spaceServiceProxy: SpaceServiceProxyProtocol + private let isChildFlow: Bool + + private let mediaProvider: MediaProviderProtocol + + private let navigationStackCoordinator: NavigationStackCoordinator + private let userIndicatorController: UserIndicatorControllerProtocol + + private var childSpaceFlowCoordinator: SpaceFlowCoordinator? + + indirect enum State: StateType { + /// The state machine hasn't started. + case initial + /// The root screen for this flow. + case space + /// A child flow is in progress. + case presentingChild(childSpaceID: String, previousState: State) + } + + enum Event: EventType { + /// The flow is being started. + case start + + /// Request the presentation of a child space flow. + /// + /// The space's `SpaceRoomListProxyProtocol` must be provided in the `userInfo`. + case startChildFlow + /// Tidy-up the child flow after it has dismissed itself. + case stopChildFlow + } + + private let stateMachine: StateMachine + private var cancellables: Set = [] + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(spaceRoomListProxy: SpaceRoomListProxyProtocol, + spaceServiceProxy: SpaceServiceProxyProtocol, + isChildFlow: Bool, + mediaProvider: MediaProviderProtocol, + navigationStackCoordinator: NavigationStackCoordinator, + userIndicatorController: UserIndicatorControllerProtocol) { + self.spaceRoomListProxy = spaceRoomListProxy + self.spaceServiceProxy = spaceServiceProxy + self.isChildFlow = isChildFlow + + self.mediaProvider = mediaProvider + + self.navigationStackCoordinator = navigationStackCoordinator + self.userIndicatorController = userIndicatorController + + stateMachine = .init(state: .initial) + configureStateMachine() + } + + func start() { + stateMachine.tryEvent(.start) + } + + func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { + // There aren't any routes to this screen yet, so clear the stacks. + clearRoute(animated: animated) + } + + func clearRoute(animated: Bool) { + switch stateMachine.state { + case .initial: + break + case .space: + if isChildFlow { + navigationStackCoordinator.pop(animated: animated) + } else { + navigationStackCoordinator.setRootCoordinator(nil, animated: animated) + } + case .presentingChild: + childSpaceFlowCoordinator?.clearRoute(animated: animated) + clearRoute(animated: animated) // Re-run with the state machine back in the .space state. + } + } + + // MARK: - Private + + private func configureStateMachine() { + stateMachine.addRoutes(event: .start, transitions: [.initial => .space]) { [weak self] _ in + self?.presentSpace() + } + + stateMachine.addRouteMapping { event, fromState, userInfo in + guard event == .startChildFlow, case .space = fromState else { return nil } + guard let spaceRoomListProxy = userInfo as? SpaceRoomListProxyProtocol else { fatalError("A space proxy must be provided.") } + return .presentingChild(childSpaceID: spaceRoomListProxy.spaceRoom.id, previousState: fromState) + } handler: { [weak self] context in + guard let self, let spaceRoomListProxy = context.userInfo as? SpaceRoomListProxyProtocol else { return } + startChildFlow(for: spaceRoomListProxy) + } + + stateMachine.addRouteMapping { event, fromState, _ in + guard event == .stopChildFlow, case .presentingChild(_, let previousState) = fromState else { return nil } + return previousState + } handler: { [weak self] _ in + guard let self else { return } + childSpaceFlowCoordinator = nil + } + + stateMachine.addErrorHandler { context in + fatalError("Unexpected transition: \(context)") + } + } + + private func presentSpace() { + let parameters = SpaceScreenCoordinatorParameters(spaceRoomListProxy: spaceRoomListProxy, + spaceServiceProxy: spaceServiceProxy, + mediaProvider: mediaProvider, + userIndicatorController: userIndicatorController) + let coordinator = SpaceScreenCoordinator(parameters: parameters) + coordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + switch action { + case .selectSpace(let spaceRoomListProxy): + stateMachine.tryEvent(.startChildFlow, userInfo: spaceRoomListProxy) + } + } + .store(in: &cancellables) + + if isChildFlow { + navigationStackCoordinator.push(coordinator) { [weak self] in + self?.actionsSubject.send(.finished) + } + } else { + navigationStackCoordinator.setRootCoordinator(coordinator) { [weak self] in + self?.actionsSubject.send(.finished) + } + } + } + + // MARK: - Other flows + + private func startChildFlow(for spaceRoomListProxy: SpaceRoomListProxyProtocol) { + let coordinator = SpaceFlowCoordinator(spaceRoomListProxy: spaceRoomListProxy, + spaceServiceProxy: spaceServiceProxy, + isChildFlow: true, + mediaProvider: mediaProvider, + navigationStackCoordinator: navigationStackCoordinator, + userIndicatorController: userIndicatorController) + + coordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .finished: + stateMachine.tryEvent(.stopChildFlow) + } + } + .store(in: &cancellables) + + childSpaceFlowCoordinator = coordinator + coordinator.start() + } +} diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 9bbf8d444..40cbe62d9 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -271,7 +271,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { .store(in: &cancellables) appSettings.$spacesEnabled - .map { $0 ? .automatic : .hidden } + .combineLatest(userSession.clientProxy.spaceService.joinedSpacesPublisher) + .map { $0 && !$1.isEmpty ? .automatic : .hidden } .weakAssign(to: \.chatsTabDetails.barVisibility, on: self) .store(in: &cancellables) } diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 0a85c8216..d98f4b044 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -85,6 +85,8 @@ extension ClientProxyMock { secureBackupController = SecureBackupControllerMock(.init(recoveryState: configuration.recoveryState)) resetIdentityReturnValue = .success(IdentityResetHandleSDKMock(.init())) + spaceService = SpaceServiceProxyMock(.init()) + roomForIdentifierClosure = { [weak self] identifier in guard let room = self?.roomSummaryProvider.roomListPublisher.value.first(where: { $0.id == identifier }) else { return nil diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index fd034d47d..767d7cfad 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2156,6 +2156,11 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { } var underlyingSecureBackupController: SecureBackupControllerProtocol! var sessionVerificationController: SessionVerificationControllerProxyProtocol? + var spaceService: SpaceServiceProxyProtocol { + get { return underlyingSpaceService } + set(value) { underlyingSpaceService = value } + } + var underlyingSpaceService: SpaceServiceProxyProtocol! var isReportRoomSupportedCallsCount = 0 var isReportRoomSupportedCalled: Bool { return isReportRoomSupportedCallsCount > 0 diff --git a/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift b/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift index 0a1ee927c..8faaa1a49 100644 --- a/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift @@ -99,6 +99,14 @@ extension [SpaceRoomProxyProtocol] { makeSpaceRooms(isSpace: true) + makeSpaceRooms(isSpace: false) } + static var mockSingleRoom: [SpaceRoomProxyProtocol] { + [SpaceRoomProxyMock(.init(id: "!spaceroom:matrix.org", + name: "Management", + isSpace: false, + joinedMembersCount: 12, + topic: "This is where everything gets organised 📋."))] + } + private static func makeSpaceRooms(isSpace: Bool) -> [SpaceRoomProxyMock] { let typeName = isSpace ? "Space" : "Room" diff --git a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift index 93dd661f6..aceaaa075 100644 --- a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift @@ -5,6 +5,7 @@ // Please see LICENSE files in the repository root for full details. // +import Combine import Foundation import MatrixRustSDK @@ -27,3 +28,16 @@ extension SpaceServiceProxyMock { } } } + +extension SpaceServiceProxyMock.Configuration { + static var populated: SpaceServiceProxyMock.Configuration { + let spaceRoomLists = [SpaceRoomProxyProtocol].mockJoinedSpaces.map { + ($0.id, SpaceRoomListProxyMock(.init(spaceRoomProxy: $0, initialSpaceRooms: .mockSpaceList))) + } + let subSpaceRoomLists = [SpaceRoomProxyProtocol].mockSpaceList.map { + ($0.id, SpaceRoomListProxyMock(.init(spaceRoomProxy: $0, initialSpaceRooms: .mockSingleRoom))) + } + + return .init(joinedSpaces: .mockJoinedSpaces, spaceRoomLists: .init(uniqueKeysWithValues: spaceRoomLists + subSpaceRoomLists)) + } +} diff --git a/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift b/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift index a176a079d..402e1ab78 100644 --- a/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift +++ b/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift @@ -12,7 +12,7 @@ import SwiftUI struct SpaceRoomCell: View { @Environment(\.dynamicTypeSize) var dynamicTypeSize - let spaceRoom: SpaceRoomProxyProtocol + let spaceRoomProxy: SpaceRoomProxyProtocol let isSelected: Bool let mediaProvider: MediaProviderProtocol! @@ -23,25 +23,25 @@ struct SpaceRoomCell: View { private let horizontalInsets = 16.0 private var subtitle: String { - if spaceRoom.isSpace { - spaceRoom.joinRule == .public ? L10n.commonPublicSpace : L10n.commonPrivateSpace + if spaceRoomProxy.isSpace { + spaceRoomProxy.joinRule == .public ? L10n.commonPublicSpace : L10n.commonPrivateSpace } else { - L10n.commonMemberCount(spaceRoom.joinedMembersCount) + L10n.commonMemberCount(spaceRoomProxy.joinedMembersCount) } } private var details: String { - if spaceRoom.isSpace { - L10n.screenSpaceListDetails(L10n.commonRooms(spaceRoom.childrenCount), - L10n.commonMemberCount(spaceRoom.joinedMembersCount)) + if spaceRoomProxy.isSpace { + L10n.screenSpaceListDetails(L10n.commonRooms(spaceRoomProxy.childrenCount), + L10n.commonMemberCount(spaceRoomProxy.joinedMembersCount)) } else { - spaceRoom.topic ?? " " // Use a single space to reserve a consistent amount of space. + spaceRoomProxy.topic ?? " " // Use a single space to reserve a consistent amount of space. } } var body: some View { Button { - action(.select(spaceRoom)) + action(.select(spaceRoomProxy)) } label: { HStack(spacing: 16.0) { avatar @@ -59,13 +59,13 @@ struct SpaceRoomCell: View { .accessibilityElement(children: .combine) } .buttonStyle(SpaceRoomCellButtonStyle(isSelected: isSelected)) - .accessibilityIdentifier(A11yIdentifiers.spaceListScreen.spaceRoomName(spaceRoom.name ?? spaceRoom.id)) + .accessibilityIdentifier(A11yIdentifiers.spaceListScreen.spaceRoomName(spaceRoomProxy.name ?? spaceRoomProxy.id)) } @ViewBuilder @MainActor private var avatar: some View { if dynamicTypeSize < .accessibility3 { - RoomAvatarImage(avatar: spaceRoom.avatar, + RoomAvatarImage(avatar: spaceRoomProxy.avatar, avatarSize: .room(on: .spaces), mediaProvider: mediaProvider) .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) @@ -76,7 +76,7 @@ struct SpaceRoomCell: View { private var content: some View { HStack(spacing: 16) { VStack(alignment: .leading, spacing: 2) { - Text(spaceRoom.name ?? spaceRoom.id) + Text(spaceRoomProxy.name ?? spaceRoomProxy.id) .font(.compound.bodyLGSemibold) .foregroundColor(.compound.textPrimary) .lineLimit(1) @@ -101,7 +101,7 @@ struct SpaceRoomCell: View { .foregroundStyle(.compound.textSecondary) .lineLimit(1) } icon: { - CompoundIcon(spaceRoom.joinRule == .public ? \.public : \.lockSolid, + CompoundIcon(spaceRoomProxy.joinRule == .public ? \.public : \.lockSolid, size: .xSmall, relativeTo: .compound.bodyMD) .foregroundStyle(.compound.iconTertiary) @@ -111,9 +111,9 @@ struct SpaceRoomCell: View { @ViewBuilder private var accessory: some View { - switch spaceRoom.state { + switch spaceRoomProxy.state { case .none, .left, .invited: - Button(L10n.actionJoin) { action(.join(spaceRoom)) } + Button(L10n.actionJoin) { action(.join(spaceRoomProxy)) } .font(.compound.bodyLG) .foregroundStyle(.compound.textActionAccent) case .joined, .knocked, .banned: @@ -141,7 +141,7 @@ struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview { static var previews: some View { VStack(spacing: 0) { ForEach(spaces, id: \.id) { space in - SpaceRoomCell(spaceRoom: space, + SpaceRoomCell(spaceRoomProxy: space, isSelected: false, mediaProvider: mediaProvider) { _ in } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift index be1096ebd..5beadfa4d 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift @@ -12,11 +12,12 @@ import SwiftUI struct SpaceListScreenCoordinatorParameters { let userSession: UserSessionProtocol - let spaceServiceProxy: SpaceServiceProxyProtocol + let selectedSpaceSubject: CurrentValuePublisher + let userIndicatorController: UserIndicatorControllerProtocol } enum SpaceListScreenCoordinatorAction { - case selectSpace(SpaceRoomProxyProtocol) + case selectSpace(SpaceRoomListProxyProtocol) case showSettings } @@ -35,7 +36,8 @@ final class SpaceListScreenCoordinator: CoordinatorProtocol { self.parameters = parameters viewModel = SpaceListScreenViewModel(userSession: parameters.userSession, - spaceServiceProxy: parameters.spaceServiceProxy) + selectedSpaceSubject: parameters.selectedSpaceSubject, + userIndicatorController: parameters.userIndicatorController) } func start() { @@ -44,8 +46,8 @@ final class SpaceListScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .selectSpace(let spaceRoom): - actionsSubject.send(.selectSpace(spaceRoom)) + case .selectSpace(let spaceRoomListProxy): + actionsSubject.send(.selectSpace(spaceRoomListProxy)) case .showSettings: actionsSubject.send(.showSettings) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift index 9fec3ba83..bee1b2089 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift @@ -8,7 +8,7 @@ import Foundation enum SpaceListScreenViewModelAction { - case selectSpace(SpaceRoomProxyProtocol) + case selectSpace(SpaceRoomListProxyProtocol) case showSettings } @@ -18,13 +18,17 @@ struct SpaceListScreenViewState: BindableState { var userAvatarURL: URL? var joinedSpaces: [SpaceRoomProxyProtocol] - var joinedRoomsCount: Int + var selectedSpaceID: String? var bindings: SpaceListScreenViewStateBindings var subtitle: String { L10n.screenSpaceListDetails(L10n.commonSpaces(joinedSpaces.count), L10n.commonRooms(joinedRoomsCount)) } + + var joinedRoomsCount: Int { + joinedSpaces.map(\.childrenCount).reduce(0, +) + } } struct SpaceListScreenViewStateBindings { } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift index 5186e7b4e..f072f4cb0 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift @@ -12,18 +12,21 @@ typealias SpaceListScreenViewModelType = StateStoreViewModelV2 = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(userSession: UserSessionProtocol, spaceServiceProxy: SpaceServiceProxyProtocol) { - self.spaceServiceProxy = spaceServiceProxy + init(userSession: UserSessionProtocol, + selectedSpaceSubject: CurrentValuePublisher, + userIndicatorController: UserIndicatorControllerProtocol) { + spaceServiceProxy = userSession.clientProxy.spaceService + self.userIndicatorController = userIndicatorController super.init(initialViewState: SpaceListScreenViewState(userID: userSession.clientProxy.userID, joinedSpaces: spaceServiceProxy.joinedSpacesPublisher.value, - joinedRoomsCount: 0, bindings: .init()), mediaProvider: userSession.mediaProvider) @@ -32,6 +35,10 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie .weakAssign(to: \.state.joinedSpaces, on: self) .store(in: &cancellables) + selectedSpaceSubject + .weakAssign(to: \.state.selectedSpaceID, on: self) + .store(in: &cancellables) + userSession.clientProxy.userAvatarURLPublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.userAvatarURL, on: self) @@ -49,12 +56,35 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie MXLog.info("View model: received view action: \(viewAction)") switch viewAction { - case .spaceAction(.select(let spaceRoom)): - actionsSubject.send(.selectSpace(spaceRoom)) + case .spaceAction(.select(let spaceRoomProxy)): + Task { await selectSpace(spaceRoomProxy) } case .spaceAction(.join(let spaceRoom)): #warning("Implement joining.") case .showSettings: actionsSubject.send(.showSettings) } } + + // MARK: - Private + + private func selectSpace(_ spaceRoomProxy: SpaceRoomProxyProtocol) async { + switch await spaceServiceProxy.spaceRoomList(for: spaceRoomProxy) { + case .success(let spaceRoomListProxy): + actionsSubject.send(.selectSpace(spaceRoomListProxy)) + case .failure(let error): + MXLog.error("Unable to select space: \(error)") + showFailureIndicator() + } + } + + // MARK: - Indicators + + private static var failureIndicatorID: String { "\(Self.self)-Failure" } + + private func showFailureIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Self.failureIndicatorID, + type: .toast, + title: L10n.errorUnknown, + iconName: "xmark")) + } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift index 0312c6c35..c01f9b757 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift @@ -59,8 +59,8 @@ struct SpaceListScreen: View { var spaces: some View { ForEach(context.viewState.joinedSpaces, id: \.id) { spaceRoom in - SpaceRoomCell(spaceRoom: spaceRoom, - isSelected: false, + SpaceRoomCell(spaceRoomProxy: spaceRoom, + isSelected: spaceRoom.id == context.viewState.selectedSpaceID, mediaProvider: context.mediaProvider) { action in context.send(viewAction: .spaceAction(action)) } @@ -102,12 +102,13 @@ struct SpaceListScreen_Previews: PreviewProvider, TestablePreview { } } - static func makeViewModel(counterValue: Int = 0) -> SpaceListScreenViewModel { + static func makeViewModel() -> SpaceListScreenViewModel { let clientProxy = ClientProxyMock(.init()) - let userSession = UserSessionMock(.init(clientProxy: clientProxy)) - let spaceService = SpaceServiceProxyMock(.init(joinedSpaces: .mockJoinedSpaces)) + clientProxy.spaceService = SpaceServiceProxyMock(.init(joinedSpaces: .mockJoinedSpaces)) - let viewModel = SpaceListScreenViewModel(userSession: userSession, spaceServiceProxy: spaceService) + let viewModel = SpaceListScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)), + selectedSpaceSubject: .init(nil), + userIndicatorController: UserIndicatorControllerMock()) return viewModel } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift index 2b3420094..834280c86 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -12,11 +12,13 @@ import SwiftUI struct SpaceScreenCoordinatorParameters { let spaceRoomListProxy: SpaceRoomListProxyProtocol + let spaceServiceProxy: SpaceServiceProxyProtocol let mediaProvider: MediaProviderProtocol + let userIndicatorController: UserIndicatorControllerProtocol } enum SpaceScreenCoordinatorAction { - case selectSpace(SpaceRoomProxyProtocol) + case selectSpace(SpaceRoomListProxyProtocol) } final class SpaceScreenCoordinator: CoordinatorProtocol { @@ -33,7 +35,10 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { init(parameters: SpaceScreenCoordinatorParameters) { self.parameters = parameters - viewModel = SpaceScreenViewModel(spaceRoomList: parameters.spaceRoomListProxy, mediaProvider: parameters.mediaProvider) + viewModel = SpaceScreenViewModel(spaceRoomList: parameters.spaceRoomListProxy, + spaceServiceProxy: parameters.spaceServiceProxy, + mediaProvider: parameters.mediaProvider, + userIndicatorController: parameters.userIndicatorController) } func start() { @@ -42,8 +47,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .selectSpace(let spaceRoom): - actionsSubject.send(.selectSpace(spaceRoom)) + case .selectSpace(let spaceRoomListProxy): + actionsSubject.send(.selectSpace(spaceRoomListProxy)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index c8348f136..bb6fee4ce 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -8,7 +8,7 @@ import Foundation enum SpaceScreenViewModelAction { - case selectSpace(SpaceRoomProxyProtocol) + case selectSpace(SpaceRoomListProxyProtocol) } struct SpaceScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index 3e543a906..c24b76fc6 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -11,12 +11,21 @@ import SwiftUI typealias SpaceScreenViewModelType = StateStoreViewModelV2 class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtocol { + private let spaceServiceProxy: SpaceServiceProxyProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(spaceRoomList: SpaceRoomListProxyProtocol, mediaProvider: MediaProviderProtocol) { + init(spaceRoomList: SpaceRoomListProxyProtocol, + spaceServiceProxy: SpaceServiceProxyProtocol, + mediaProvider: MediaProviderProtocol, + userIndicatorController: UserIndicatorControllerProtocol) { + self.spaceServiceProxy = spaceServiceProxy + self.userIndicatorController = userIndicatorController + super.init(initialViewState: SpaceScreenViewState(space: spaceRoomList.spaceRoom, rooms: spaceRoomList.spaceRoomsPublisher.value), mediaProvider: mediaProvider) @@ -47,14 +56,37 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc MXLog.info("View model: received view action: \(viewAction)") switch viewAction { - case .spaceAction(.select(let spaceRoom)): - if spaceRoom.isSpace { - actionsSubject.send(.selectSpace(spaceRoom)) + case .spaceAction(.select(let spaceRoomProxy)): + if spaceRoomProxy.isSpace { + Task { await selectSpace(spaceRoomProxy) } } else { - #warning("Implement joining") + #warning("Implement room flow") } case .spaceAction(.join(let spaceID)): #warning("Implement joining.") } } + + // MARK: - Private + + private func selectSpace(_ spaceRoomProxy: SpaceRoomProxyProtocol) async { + switch await spaceServiceProxy.spaceRoomList(for: spaceRoomProxy) { + case .success(let spaceRoomListProxy): + actionsSubject.send(.selectSpace(spaceRoomListProxy)) + case .failure(let error): + MXLog.error("Unable to select space: \(error)") + showFailureIndicator() + } + } + + // MARK: - Indicators + + private static var failureIndicatorID: String { "\(Self.self)-Failure" } + + private func showFailureIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Self.failureIndicatorID, + type: .toast, + title: L10n.errorUnknown, + iconName: "xmark")) + } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift index 9c961a8dd..ec50e8e85 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift @@ -26,8 +26,8 @@ struct SpaceScreen: View { @ViewBuilder var rooms: some View { - ForEach(context.viewState.rooms, id: \.id) { spaceRoom in - SpaceRoomCell(spaceRoom: spaceRoom, + ForEach(context.viewState.rooms, id: \.id) { spaceRoomProxy in + SpaceRoomCell(spaceRoomProxy: spaceRoomProxy, isSelected: false, mediaProvider: context.mediaProvider) { action in context.send(viewAction: .spaceAction(action)) @@ -65,7 +65,9 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview { initialSpaceRooms: .mockSpaceList)) let viewModel = SpaceScreenViewModel(spaceRoomList: spaceRoomListProxy, - mediaProvider: MediaProviderMock(configuration: .init())) + spaceServiceProxy: SpaceServiceProxyMock(.init()), + mediaProvider: MediaProviderMock(configuration: .init()), + userIndicatorController: UserIndicatorControllerMock()) return viewModel } } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index eef2270a6..31d44f9e6 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -57,6 +57,8 @@ class ClientProxy: ClientProxyProtocol { private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol? + let spaceService: SpaceServiceProxyProtocol + private static var roomCreationPowerLevelOverrides: PowerLevels { .init(usersDefault: nil, eventsDefault: nil, @@ -164,6 +166,10 @@ class ClientProxy: ClientProxyProtocol { secureBackupController = SecureBackupController(encryption: client.encryption()) + // Temporarily using the mock until the SDK is updated. + // spaceService = SpaceServiceProxy(spaceService: client.spaceService()) + spaceService = SpaceServiceProxyMock(.init()) + let configuredAppService = try await ClientProxyServices(client: client, actionsSubject: actionsSubject, notificationSettings: notificationSettings, diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 7e04b4d69..a4bc00157 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -121,6 +121,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get } + var spaceService: SpaceServiceProxyProtocol { get } + var isReportRoomSupported: Bool { get async } var isLiveKitRTCSupported: Bool { get async } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 5ee8bee62..b37fbe368 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -577,14 +577,19 @@ class MockScreen: Identifiable { appSettings: ServiceLocator.shared.settings, mediaProvider: MediaProviderMock(configuration: .init())) return SessionVerificationScreenCoordinator(parameters: parameters) - case .userSessionScreen, .userSessionScreenReply: + case .userSessionScreen, .userSessionScreenReply, .userSessionSpacesFlow: let appSettings: AppSettings = ServiceLocator.shared.settings appSettings.hasRunIdentityConfirmationOnboarding = true appSettings.hasRunNotificationPermissionsOnboarding = true appSettings.analyticsConsentState = .optedOut + appSettings.spacesEnabled = true let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", deviceID: "MOCKCLIENT", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))))) + // The tab bar remains hidden for the non-spaces tests as we don't supply any mock spaces. + let spaceServiceProxy = SpaceServiceProxyMock(id == .userSessionSpacesFlow ? .populated : .init()) + clientProxy.spaceService = spaceServiceProxy + let appMediator = AppMediatorMock.default appMediator.underlyingWindowManager = windowManager diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index f0e383092..cae3b08b9 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -47,6 +47,7 @@ enum UITestsScreenIdentifier: String { case startChatWithSearchResults case userSessionScreen case userSessionScreenReply + case userSessionSpacesFlow case autoUpdatingTimeline } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-en-GB-0.png index 5da5c061b..869ca2bb5 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c569e1d6d8af58bfe5e80ef069c4003bec9cbf9d5ed392c1f4004a678f847d2 -size 192352 +oid sha256:e429a65ea8ef31f50dc71b33fe90ef953783381eda4af3cff9d64756807f9fd0 +size 193511 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-en-GB-0.png index 6d97e2584..8170c40da 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a096fa0a28ac3032ce18ff7ab44278abefb6634a285df81125c268f6ee04ed88 -size 136997 +oid sha256:9301fc1565670473d8a3c7d4c084fd896f979e44827360c97713eef1f537417c +size 137771 diff --git a/UITests/Sources/UserSessionScreenTests.swift b/UITests/Sources/UserSessionScreenTests.swift index a6c912750..983ace1fc 100644 --- a/UITests/Sources/UserSessionScreenTests.swift +++ b/UITests/Sources/UserSessionScreenTests.swift @@ -10,21 +10,33 @@ import XCTest @MainActor class UserSessionScreenTests: XCTestCase { let firstRoomName = "Foundation 🔭🪐🌌" + let firstSpaceName = "The Foundation" + let firstSubspaceName = "Company Space" + + enum Step { + static let homeScreen = 1 + static let roomScreen = 2 + static let composerAttachments = 3 + static let spacesTabBar = 4 + static let spaceList = 5 + static let spaceScreen = 6 + static let subspaceScreen = 7 + } func testUserSessionFlows() async throws { let app = Application.launch(.userSessionScreen) app.swipeDown() // Make sure the header shows a large title - try await app.assertScreenshot(step: 1) + try await app.assertScreenshot(step: Step.homeScreen) app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() XCTAssert(app.staticTexts[firstRoomName].waitForExistence(timeout: 5.0)) try await Task.sleep(for: .seconds(1)) - try await app.assertScreenshot(step: 2) + try await app.assertScreenshot(step: Step.roomScreen) app.buttons[A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions].tap(.center) - try await app.assertScreenshot(step: 3) + try await app.assertScreenshot(step: Step.composerAttachments) } func testUserSessionReply() async throws { @@ -53,4 +65,25 @@ class UserSessionScreenTests: XCTestCase { let joinButton = app.buttons["Continue"] XCTAssert(joinButton.waitForExistence(timeout: 10)) } + + func testSpaceExploration() async throws { + let app = Application.launch(.userSessionSpacesFlow) + + try await app.assertScreenshot(step: Step.spacesTabBar) + + // app.tabBars doesn't work on iPadOS 18 😐 + app.buttons["Spaces"].firstMatch.tap(.center) + + try await app.assertScreenshot(step: Step.spaceList) + + app.buttons[A11yIdentifiers.spaceListScreen.spaceRoomName(firstSpaceName)].tap() + XCTAssert(app.staticTexts[firstSpaceName].waitForExistence(timeout: 5.0)) + try await Task.sleep(for: .seconds(1)) + try await app.assertScreenshot(step: Step.spaceScreen) + + app.buttons[A11yIdentifiers.spaceListScreen.spaceRoomName(firstSubspaceName)].tap() + XCTAssert(app.staticTexts[firstSubspaceName].waitForExistence(timeout: 5.0)) + try await Task.sleep(for: .seconds(1)) + try await app.assertScreenshot(step: Step.subspaceScreen) + } } diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-4.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-4.png new file mode 100644 index 000000000..cd7df060f --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dee57958e736136e2598db20aae58486bf68d56352c68097319e3f4e2cea6dd9 +size 452748 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-5.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-5.png new file mode 100644 index 000000000..d5a0b80e8 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58e3b99a771576265452df3bc51eef487ecf160e48f5ce9b4dd507fe0892e6e4 +size 402961 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-6.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-6.png new file mode 100644 index 000000000..6e8253e4f --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63044eae82b5caf0e61eaa7e65f2fff30e2dca8fe282ce7c7fd2e4edd562811d +size 373691 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-7.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-7.png new file mode 100644 index 000000000..b925c17fa --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-18-5-en-GB-7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d16db9f4d2cab604bc19a76585202586df21749d48da09c8e969c2624637ccf +size 277095 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-4.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-4.png new file mode 100644 index 000000000..4539d353d --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f3668e91005046ed8ee295d34c4f160a1501e427337c6bf96cdcdf34aa83c65 +size 484663 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-5.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-5.png new file mode 100644 index 000000000..55a8b75b5 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:959f56d7a54cb10cd0fbdaac3fab6e65fab0d2d391f21211be35ea312d7c6543 +size 332003 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-6.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-6.png new file mode 100644 index 000000000..a7a4ede5d --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d966e5361b6c386668228ee436c1fbf4a2b53065169120aaca669ac485736c5f +size 402376 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-7.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-7.png new file mode 100644 index 000000000..d0757933f --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-18-5-en-GB-7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10c0b735d009691762e8719ff2dc3c01ce8e1c984e080caebe322336a6d378bd +size 117024 diff --git a/UnitTests/Sources/SpaceListScreenViewModelTests.swift b/UnitTests/Sources/SpaceListScreenViewModelTests.swift index 94025a919..b917f76c7 100644 --- a/UnitTests/Sources/SpaceListScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceListScreenViewModelTests.swift @@ -52,7 +52,7 @@ class SpaceListScreenViewModelTests: XCTestCase { let action = try await deferred.fulfill() switch action { - case .selectSpace(let spaceRoomProxy) where spaceRoomProxy.id == selectedSpace.id: + case .selectSpace(let spaceRoomListProxy) where spaceRoomListProxy.spaceRoom.id == selectedSpace.id: break default: XCTFail("The action should select the space.") @@ -72,7 +72,11 @@ class SpaceListScreenViewModelTests: XCTestCase { ]) spaceServiceProxy = SpaceServiceProxyMock(.init()) spaceServiceProxy.joinedSpacesPublisher = joinedSpacesSubject.asCurrentValuePublisher() + spaceServiceProxy.spaceRoomListForClosure = { .success(SpaceRoomListProxyMock(.init(spaceRoomProxy: $0))) } + clientProxy.spaceService = spaceServiceProxy - viewModel = SpaceListScreenViewModel(userSession: userSession, spaceServiceProxy: spaceServiceProxy) + viewModel = SpaceListScreenViewModel(userSession: userSession, + selectedSpaceSubject: .init(nil), + userIndicatorController: UserIndicatorControllerMock()) } } diff --git a/UnitTests/Sources/SpaceScreenViewModelTests.swift b/UnitTests/Sources/SpaceScreenViewModelTests.swift index ffd2326ca..4b91b148b 100644 --- a/UnitTests/Sources/SpaceScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceScreenViewModelTests.swift @@ -97,7 +97,7 @@ class SpaceScreenViewModelTests: XCTestCase { let action = try await deferred.fulfill() switch action { - case .selectSpace(let spaceRoomProxy) where spaceRoomProxy.id == selectedSpace.id: + case .selectSpace(let spaceRoomListProxy) where spaceRoomListProxy.spaceRoom.id == selectedSpace.id: break default: XCTFail("The action should select the space.") @@ -111,7 +111,12 @@ class SpaceScreenViewModelTests: XCTestCase { paginationStateSubject: paginationStateSubject, paginationResponses: paginationResponses)) + let spaceServiceProxy = SpaceServiceProxyMock(.init()) + spaceServiceProxy.spaceRoomListForClosure = { .success(SpaceRoomListProxyMock(.init(spaceRoomProxy: $0))) } + viewModel = SpaceScreenViewModel(spaceRoomList: spaceRoomListProxy, - mediaProvider: MediaProviderMock(configuration: .init())) + spaceServiceProxy: spaceServiceProxy, + mediaProvider: MediaProviderMock(configuration: .init()), + userIndicatorController: UserIndicatorControllerMock()) } }