diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 473b6d204..43ab18e55 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -703,7 +703,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { guard let self else { return } switch action { - case .joined: + case .joined(.roomID(let roomID)): Task { [weak self] in guard let self else { return } @@ -718,6 +718,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated)) } } + case .joined(.space): + #warning("The space flow should be shown here.") case .cancelled: stateMachine.tryEvent(.dismissJoinRoomScreen) case .presentDeclineAndBlock(let userID): diff --git a/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift index 802408fc8..a4b06a20c 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift @@ -134,7 +134,7 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol { } private func startSpaceFlow(spaceRoomListProxy: SpaceRoomListProxyProtocol) { - let coordinator = SpaceFlowCoordinator(spaceRoomListProxy: spaceRoomListProxy, + let coordinator = SpaceFlowCoordinator(entryPoint: .space(spaceRoomListProxy), spaceServiceProxy: userSession.clientProxy.spaceService, isChildFlow: false, navigationStackCoordinator: detailNavigationStackCoordinator, diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index 48f76cf1b..849f780d6 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -15,8 +15,20 @@ enum SpaceFlowCoordinatorAction { case finished } +enum SpaceFlowCoordinatorEntryPoint { + case space(SpaceRoomListProxyProtocol) + case joinSpace(SpaceRoomProxyProtocol) + + var spaceID: String { + switch self { + case .space(let spaceRoomListProxy): spaceRoomListProxy.spaceRoomProxy.id + case .joinSpace(let spaceRoomProxy): spaceRoomProxy.id + } + } +} + class SpaceFlowCoordinator: FlowCoordinatorProtocol { - private let spaceRoomListProxy: SpaceRoomListProxyProtocol + private var entryPoint: SpaceFlowCoordinatorEntryPoint private let spaceServiceProxy: SpaceServiceProxyProtocol private let isChildFlow: Bool @@ -32,6 +44,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { indirect enum State: StateType { /// The state machine hasn't started. case initial + /// Shown when the flow is started for an unjoined space. + case joinSpace /// The root screen for this flow. case space /// A child (space) flow is in progress. @@ -43,6 +57,11 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { enum Event: EventType { /// The flow is being started. case start + /// The flow is being started for an unjoined space. + case startUnjoined + + /// The join space screen joined the space. + case joinedSpace /// Request the presentation of a child space flow. /// @@ -63,12 +82,12 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.eraseToAnyPublisher() } - init(spaceRoomListProxy: SpaceRoomListProxyProtocol, + init(entryPoint: SpaceFlowCoordinatorEntryPoint, spaceServiceProxy: SpaceServiceProxyProtocol, isChildFlow: Bool, navigationStackCoordinator: NavigationStackCoordinator, flowParameters: CommonFlowParameters) { - self.spaceRoomListProxy = spaceRoomListProxy + self.entryPoint = entryPoint self.spaceServiceProxy = spaceServiceProxy self.isChildFlow = isChildFlow self.flowParameters = flowParameters @@ -80,7 +99,12 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { } func start() { - stateMachine.tryEvent(.start) + switch entryPoint { + case .space: + stateMachine.tryEvent(.start) + case .joinSpace: + stateMachine.tryEvent(.startUnjoined) + } } func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { @@ -92,7 +116,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { switch stateMachine.state { case .initial: break - case .space: + case .joinSpace, .space: if isChildFlow { navigationStackCoordinator.pop(animated: animated) } else { @@ -114,13 +138,21 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { self?.presentSpace() } + stateMachine.addRoutes(event: .startUnjoined, transitions: [.initial => .joinSpace]) { [weak self] _ in + self?.presentJoinSpaceScreen() + } + + stateMachine.addRoutes(event: .joinedSpace, transitions: [.joinSpace => .space]) { [weak self] _ in + self?.presentSpaceAfterJoining() + } + 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.spaceRoomProxy.id, previousState: fromState) + guard let childEntryPoint = userInfo as? SpaceFlowCoordinatorEntryPoint else { fatalError("An entry point must be provided.") } + return .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: fromState) } handler: { [weak self] context in - guard let self, let spaceRoomListProxy = context.userInfo as? SpaceRoomListProxyProtocol else { return } - startChildFlow(for: spaceRoomListProxy) + guard let self, let entryPoint = context.userInfo as? SpaceFlowCoordinatorEntryPoint else { return } + startChildFlow(with: entryPoint) } stateMachine.addRouteMapping { event, fromState, _ in @@ -155,6 +187,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { } private func presentSpace() { + guard case let .space(spaceRoomListProxy) = entryPoint else { fatalError("Attempting to show a space with the wrong entry point.") } + let parameters = SpaceScreenCoordinatorParameters(spaceRoomListProxy: spaceRoomListProxy, spaceServiceProxy: spaceServiceProxy, selectedSpaceRoomPublisher: selectedSpaceRoomSubject.asCurrentValuePublisher(), @@ -166,7 +200,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { guard let self else { return } switch action { case .selectSpace(let spaceRoomListProxy): - stateMachine.tryEvent(.startChildFlow, userInfo: spaceRoomListProxy) + stateMachine.tryEvent(.startChildFlow, userInfo: SpaceFlowCoordinatorEntryPoint.space(spaceRoomListProxy)) + case .selectUnjoinedSpace(let spaceRoomProxy): + stateMachine.tryEvent(.startChildFlow, userInfo: SpaceFlowCoordinatorEntryPoint.joinSpace(spaceRoomProxy)) case .selectRoom(let roomID): stateMachine.tryEvent(.startRoomFlow(roomID: roomID)) } @@ -184,10 +220,61 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { } } + private func presentJoinSpaceScreen() { + guard case let .joinSpace(spaceRoomProxy) = entryPoint else { fatalError("Attempting to join a space with the wrong entry point.") } + + let parameters = JoinRoomScreenCoordinatorParameters(roomID: spaceRoomProxy.id, + via: [], + userSession: flowParameters.userSession, + userIndicatorController: flowParameters.userIndicatorController, + appSettings: flowParameters.appSettings) + let coordinator = JoinRoomScreenCoordinator(parameters: parameters) + coordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + switch action { + case .joined(.space(let spaceRoomListProxy)): + entryPoint = .space(spaceRoomListProxy) + stateMachine.tryEvent(.joinedSpace) + case .joined(.roomID): + MXLog.error("Expected to join a space, but got a room ID instead.") + clearRoute(animated: true) + case .cancelled: + clearRoute(animated: true) + case .presentDeclineAndBlock: + MXLog.error("Joining a space from the spaces tab shouldn't involve an inviter.") + clearRoute(animated: true) + } + } + .store(in: &cancellables) + + if isChildFlow { + navigationStackCoordinator.push(coordinator) { [weak self] in + guard let self, stateMachine.state == .joinSpace else { return } + actionsSubject.send(.finished) + } + } else { + navigationStackCoordinator.setRootCoordinator(coordinator) { [weak self] in + guard let self, stateMachine.state == .joinSpace else { return } + actionsSubject.send(.finished) + } + } + } + + private func presentSpaceAfterJoining() { + if isChildFlow { + navigationStackCoordinator.pop() + } else { + navigationStackCoordinator.setRootCoordinator(nil) + } + + presentSpace() + } + // MARK: - Other flows - private func startChildFlow(for spaceRoomListProxy: SpaceRoomListProxyProtocol) { - let coordinator = SpaceFlowCoordinator(spaceRoomListProxy: spaceRoomListProxy, + private func startChildFlow(with entryPoint: SpaceFlowCoordinatorEntryPoint) { + let coordinator = SpaceFlowCoordinator(entryPoint: entryPoint, spaceServiceProxy: spaceServiceProxy, isChildFlow: true, navigationStackCoordinator: navigationStackCoordinator, @@ -210,7 +297,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { childSpaceFlowCoordinator = coordinator coordinator.start() - selectedSpaceRoomSubject.send(spaceRoomListProxy.spaceRoomProxy.id) + selectedSpaceRoomSubject.send(entryPoint.spaceID) } private func startRoomFlow(roomID: String) { diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 358057a4b..146b24526 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -15900,15 +15900,15 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable { //MARK: - spaceRoomList - var spaceRoomListForUnderlyingCallsCount = 0 - var spaceRoomListForCallsCount: Int { + var spaceRoomListSpaceIDUnderlyingCallsCount = 0 + var spaceRoomListSpaceIDCallsCount: Int { get { if Thread.isMainThread { - return spaceRoomListForUnderlyingCallsCount + return spaceRoomListSpaceIDUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = spaceRoomListForUnderlyingCallsCount + returnValue = spaceRoomListSpaceIDUnderlyingCallsCount } return returnValue! @@ -15916,29 +15916,29 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable { } set { if Thread.isMainThread { - spaceRoomListForUnderlyingCallsCount = newValue + spaceRoomListSpaceIDUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - spaceRoomListForUnderlyingCallsCount = newValue + spaceRoomListSpaceIDUnderlyingCallsCount = newValue } } } } - var spaceRoomListForCalled: Bool { - return spaceRoomListForCallsCount > 0 + var spaceRoomListSpaceIDCalled: Bool { + return spaceRoomListSpaceIDCallsCount > 0 } - var spaceRoomListForReceivedSpaceRoomProxy: SpaceRoomProxyProtocol? - var spaceRoomListForReceivedInvocations: [SpaceRoomProxyProtocol] = [] + var spaceRoomListSpaceIDReceivedSpaceID: String? + var spaceRoomListSpaceIDReceivedInvocations: [String] = [] - var spaceRoomListForUnderlyingReturnValue: Result! - var spaceRoomListForReturnValue: Result! { + var spaceRoomListSpaceIDUnderlyingReturnValue: Result! + var spaceRoomListSpaceIDReturnValue: Result! { get { if Thread.isMainThread { - return spaceRoomListForUnderlyingReturnValue + return spaceRoomListSpaceIDUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = spaceRoomListForUnderlyingReturnValue + returnValue = spaceRoomListSpaceIDUnderlyingReturnValue } return returnValue! @@ -15946,26 +15946,26 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable { } set { if Thread.isMainThread { - spaceRoomListForUnderlyingReturnValue = newValue + spaceRoomListSpaceIDUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - spaceRoomListForUnderlyingReturnValue = newValue + spaceRoomListSpaceIDUnderlyingReturnValue = newValue } } } } - var spaceRoomListForClosure: ((SpaceRoomProxyProtocol) async -> Result)? + var spaceRoomListSpaceIDClosure: ((String) async -> Result)? - func spaceRoomList(for spaceRoomProxy: SpaceRoomProxyProtocol) async -> Result { - spaceRoomListForCallsCount += 1 - spaceRoomListForReceivedSpaceRoomProxy = spaceRoomProxy + func spaceRoomList(spaceID: String) async -> Result { + spaceRoomListSpaceIDCallsCount += 1 + spaceRoomListSpaceIDReceivedSpaceID = spaceID DispatchQueue.main.async { - self.spaceRoomListForReceivedInvocations.append(spaceRoomProxy) + self.spaceRoomListSpaceIDReceivedInvocations.append(spaceID) } - if let spaceRoomListForClosure = spaceRoomListForClosure { - return await spaceRoomListForClosure(spaceRoomProxy) + if let spaceRoomListSpaceIDClosure = spaceRoomListSpaceIDClosure { + return await spaceRoomListSpaceIDClosure(spaceID) } else { - return spaceRoomListForReturnValue + return spaceRoomListSpaceIDReturnValue } } } diff --git a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift index 423a9b0e6..f9b3c1dc2 100644 --- a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift @@ -19,8 +19,8 @@ extension SpaceServiceProxyMock { self.init() joinedSpacesPublisher = .init(configuration.joinedSpaces) - spaceRoomListForClosure = { spaceRoomProxy in - if let spaceRoomList = configuration.spaceRoomLists[spaceRoomProxy.id] { + spaceRoomListSpaceIDClosure = { spaceID in + if let spaceRoomList = configuration.spaceRoomLists[spaceID] { .success(spaceRoomList) } else { .failure(.sdkError(ClientProxyMockError.generic)) diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift index 60f8d6a96..db404a85d 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift @@ -16,8 +16,13 @@ struct JoinRoomScreenCoordinatorParameters { let appSettings: AppSettings } +enum JoinRoomScreenJoinDetails { + case roomID(String) + case space(SpaceRoomListProxyProtocol) +} + enum JoinRoomScreenCoordinatorAction { - case joined + case joined(JoinRoomScreenJoinDetails) case cancelled case presentDeclineAndBlock(userID: String) } @@ -46,8 +51,8 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .joined: - actionsSubject.send(.joined) + case .joined(let details): + actionsSubject.send(.joined(details)) case .dismiss: actionsSubject.send(.cancelled) case .presentDeclineAndBlock(let userID): diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift index 283d9ee9e..c7510242d 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift @@ -7,8 +7,8 @@ import Foundation -enum JoinRoomScreenViewModelAction: Equatable { - case joined +enum JoinRoomScreenViewModelAction { + case joined(JoinRoomScreenJoinDetails) case dismiss case presentDeclineAndBlock(userID: String) } @@ -43,6 +43,7 @@ struct JoinRoomScreenRoomDetails { let memberCount: Int? let inviter: RoomInviterDetails? let isDirect: Bool? + let isSpace: Bool? } struct JoinRoomScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift index 0e0ef4a5f..ad49f9ed6 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift @@ -179,7 +179,8 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo avatar: avatar, memberCount: info?.joinedMembersCount, inviter: inviter, - isDirect: info?.isDirect) + isDirect: info?.isDirect, + isSpace: info?.isSpace) await updateMode() } @@ -243,8 +244,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo if let alias = state.roomDetails?.canonicalAlias { switch await clientProxy.joinRoomAlias(alias) { case .success: - appSettings.seenInvites.remove(roomID) - actionsSubject.send(.joined) + await finishJoinAction() case .failure(let error): switch error { case .forbiddenAccess: @@ -261,8 +261,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo } else { switch await clientProxy.joinRoom(roomID, via: via) { case .success: - appSettings.seenInvites.remove(roomID) - actionsSubject.send(.joined) + await finishJoinAction() case .failure(let error): switch error { case .forbiddenAccess: @@ -279,6 +278,23 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo } } + private func finishJoinAction() async { + appSettings.seenInvites.remove(roomID) + + guard state.roomDetails?.isSpace == true else { + actionsSubject.send(.joined(.roomID(roomID))) + return + } + + switch await clientProxy.spaceService.spaceRoomList(spaceID: roomID) { + case .success(let spaceRoomListProxy): + actionsSubject.send(.joined(.space(spaceRoomListProxy))) + case .failure(let error): + MXLog.error("Failed to get the space room list after joining: \(error)") + userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown)) + } + } + private func knockRoom() async { showLoadingIndicator() diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift index a5d265a77..c06ff51c7 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift @@ -58,8 +58,8 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie switch viewAction { case .spaceAction(.select(let spaceRoomProxy)): Task { await selectSpace(spaceRoomProxy) } - case .spaceAction(.join(let spaceRoomProxy)): - #warning("Implement joining.") + case .spaceAction(.join): + fatalError("There shouldn't be any unjoined spaces in the joined spaces list.") case .showSettings: actionsSubject.send(.showSettings) } @@ -68,7 +68,7 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie // MARK: - Private private func selectSpace(_ spaceRoomProxy: SpaceRoomProxyProtocol) async { - switch await spaceServiceProxy.spaceRoomList(for: spaceRoomProxy) { + switch await spaceServiceProxy.spaceRoomList(spaceID: spaceRoomProxy.id) { case .success(let spaceRoomListProxy): actionsSubject.send(.selectSpace(spaceRoomListProxy)) case .failure(let error): diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift index d8310a3ca..655a182df 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -20,6 +20,7 @@ struct SpaceScreenCoordinatorParameters { enum SpaceScreenCoordinatorAction { case selectSpace(SpaceRoomListProxyProtocol) + case selectUnjoinedSpace(SpaceRoomProxyProtocol) case selectRoom(roomID: String) } @@ -52,6 +53,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { switch action { case .selectSpace(let spaceRoomListProxy): actionsSubject.send(.selectSpace(spaceRoomListProxy)) + case .selectUnjoinedSpace(let spaceRoomProxy): + actionsSubject.send(.selectUnjoinedSpace(spaceRoomProxy)) case .selectRoom(let roomID): actionsSubject.send(.selectRoom(roomID: roomID)) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index 002f0d5e1..690bead9f 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -9,6 +9,7 @@ import Foundation enum SpaceScreenViewModelAction { case selectSpace(SpaceRoomListProxyProtocol) + case selectUnjoinedSpace(SpaceRoomProxyProtocol) case selectRoom(roomID: String) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index d0451c27a..366d21bfd 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -68,7 +68,11 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc switch viewAction { case .spaceAction(.select(let spaceRoomProxy)): if spaceRoomProxy.isSpace { - Task { await selectSpace(spaceRoomProxy) } + if spaceRoomProxy.state != .joined { + actionsSubject.send(.selectUnjoinedSpace(spaceRoomProxy)) + } else { + Task { await selectSpace(spaceRoomProxy) } + } } else { // No need to check the join state, the room flow will show an appropriately configured join screen if needed. actionsSubject.send(.selectRoom(roomID: spaceRoomProxy.id)) @@ -85,16 +89,6 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc // 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() - } - } - private func join(_ spaceRoomProxy: SpaceRoomProxyProtocol) async { state.joiningRoomIDs.insert(spaceRoomProxy.id) defer { state.joiningRoomIDs.remove(spaceRoomProxy.id) } @@ -114,6 +108,16 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc } } + private func selectSpace(_ spaceRoomProxy: SpaceRoomProxyProtocol) async { + switch await spaceServiceProxy.spaceRoomList(spaceID: spaceRoomProxy.id) { + 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" } diff --git a/ElementX/Sources/Services/Spaces/SpaceRoomListProxy.swift b/ElementX/Sources/Services/Spaces/SpaceRoomListProxy.swift index 4eaa9396d..f5e76d778 100644 --- a/ElementX/Sources/Services/Spaces/SpaceRoomListProxy.swift +++ b/ElementX/Sources/Services/Spaces/SpaceRoomListProxy.swift @@ -21,9 +21,11 @@ class SpaceRoomListProxy: SpaceRoomListProxyProtocol { private let paginationStateHandle: TaskHandle let paginationStatePublisher: CurrentValuePublisher - init(_ spaceRoomList: SpaceRoomListProtocol, spaceRoomProxy: SpaceRoomProxyProtocol) { + init(_ spaceRoomList: SpaceRoomListProtocol) throws { + guard let spaceRoom = spaceRoomList.space() else { throw SpaceRoomListProxyError.missingSpace } + self.spaceRoomList = spaceRoomList - self.spaceRoomProxy = spaceRoomProxy + spaceRoomProxy = SpaceRoomProxy(spaceRoom: spaceRoom) let paginationStateSubject = CurrentValueSubject(spaceRoomList.paginationState()) paginationStatePublisher = paginationStateSubject.asCurrentValuePublisher() diff --git a/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift b/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift index 4df22523b..e89c13142 100644 --- a/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift +++ b/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift @@ -8,6 +8,10 @@ import Combine import MatrixRustSDK +enum SpaceRoomListProxyError: Error { + case missingSpace +} + // sourcery: AutoMockable protocol SpaceRoomListProxyProtocol { var spaceRoomProxy: SpaceRoomProxyProtocol { get } diff --git a/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift b/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift index 8e10992fe..f8c8c42c7 100644 --- a/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift +++ b/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift @@ -30,11 +30,11 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol { }) } - func spaceRoomList(for spaceRoomProxy: SpaceRoomProxyProtocol) async -> Result { + func spaceRoomList(spaceID: String) async -> Result { do { - return try await .success(SpaceRoomListProxy(spaceService.spaceRoomList(spaceId: spaceRoomProxy.id), spaceRoomProxy: spaceRoomProxy)) + return try await .success(SpaceRoomListProxy(spaceService.spaceRoomList(spaceId: spaceID))) } catch { - MXLog.error("Failed creating space room list for \(spaceRoomProxy.id): \(error)") + MXLog.error("Failed creating space room list for \(spaceID): \(error)") return .failure(.sdkError(error)) } } diff --git a/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift b/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift index 709147804..aa14a7e12 100644 --- a/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift +++ b/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift @@ -9,11 +9,12 @@ import Foundation enum SpaceServiceProxyError: Error { case sdkError(Error) + case missingSpace } // sourcery: AutoMockable protocol SpaceServiceProxyProtocol { var joinedSpacesPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> { get } - func spaceRoomList(for spaceRoomProxy: SpaceRoomProxyProtocol) async -> Result + func spaceRoomList(spaceID: String) async -> Result } diff --git a/UITests/Sources/UserSessionScreenTests.swift b/UITests/Sources/UserSessionScreenTests.swift index 9dbb812ad..38d332cd4 100644 --- a/UITests/Sources/UserSessionScreenTests.swift +++ b/UITests/Sources/UserSessionScreenTests.swift @@ -11,9 +11,9 @@ import XCTest class UserSessionScreenTests: XCTestCase { let firstRoomName = "Foundation 🔭🪐🌌" let firstSpaceName = "The Foundation" - let firstSpaceRoomName = "Company Room" - let firstSubspaceName = "Company Space" - let firstSubspaceRoomName = "Management" + let unjoinedSpaceRoomName = "Company Room" + let joinedSubspaceName = "Joined Space" + let joinedSubspaceRoomName = "Management" enum Step { static let homeScreen = 1 @@ -87,24 +87,24 @@ class UserSessionScreenTests: XCTestCase { 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)) + app.buttons[A11yIdentifiers.spaceListScreen.spaceRoomName(joinedSubspaceName)].tap() + XCTAssert(app.staticTexts[joinedSubspaceName].waitForExistence(timeout: 5.0)) try await Task.sleep(for: .seconds(1)) try await app.assertScreenshot(step: Step.subspaceScreen) - app.buttons[A11yIdentifiers.spaceListScreen.spaceRoomName(firstSubspaceRoomName)].tap() - XCTAssert(app.staticTexts[firstSubspaceRoomName].waitForExistence(timeout: 5.0)) + app.buttons[A11yIdentifiers.spaceListScreen.spaceRoomName(joinedSubspaceRoomName)].tap() + XCTAssert(app.staticTexts[joinedSubspaceRoomName].waitForExistence(timeout: 5.0)) try await Task.sleep(for: .seconds(1)) try await app.assertScreenshot(step: Step.subspaceRoomScreen) - app.navigationBars.buttons[firstSubspaceName].firstMatch.tap(.center) - XCTAssert(app.staticTexts[firstSubspaceName].waitForExistence(timeout: 5.0)) + app.navigationBars.buttons[joinedSubspaceName].firstMatch.tap(.center) + XCTAssert(app.staticTexts[joinedSubspaceName].waitForExistence(timeout: 5.0)) app.navigationBars.buttons[firstSpaceName].firstMatch.tap(.center) XCTAssert(app.staticTexts[firstSpaceName].waitForExistence(timeout: 5.0)) - app.buttons[A11yIdentifiers.spaceListScreen.spaceRoomName(firstSpaceRoomName)].tap() - XCTAssert(app.staticTexts[firstSpaceRoomName].waitForExistence(timeout: 5.0)) + app.buttons[A11yIdentifiers.spaceListScreen.spaceRoomName(unjoinedSpaceRoomName)].tap() + XCTAssert(app.staticTexts[unjoinedSpaceRoomName].waitForExistence(timeout: 5.0)) try await Task.sleep(for: .seconds(1)) try await app.assertScreenshot(step: Step.spaceJoinRoomScreen) } 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 index 2131ec32a..abb456e37 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef257a3d7f03a0eaee4e4354c847c90cbc9d0f9d5c52767d1d49d200232337b0 -size 274101 +oid sha256:411d73e47a4c33cca4ad4a80f234707a11bb2090bd33d081bf0cc4702356b864 +size 277762 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 index 42bf3c92f..c0524644d 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:869be3a8f6ed1705de9fa6d37b6ce0a2e143a6f53a5b439f7cf67e93cf2d082f -size 109237 +oid sha256:1f1d0f742cd7ed94b8c20f7b4bb7d2d9aeee77b15118b04f4c14f914db142b62 +size 115249 diff --git a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift index 4f838462d..5b8de014c 100644 --- a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift +++ b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift @@ -47,7 +47,7 @@ class JoinRoomScreenViewModelTests: XCTestCase { XCTAssertTrue(appSettings.seenInvites.isEmpty, "Only an invited room should register the room ID as a seen invite.") - let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined } + let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined(.roomID("1")) } context.send(viewAction: .join) try await deferred.fulfill() } @@ -60,7 +60,7 @@ class JoinRoomScreenViewModelTests: XCTestCase { XCTAssertEqual(appSettings.seenInvites, ["1"], "The invited room's ID should be registered as a seen invite.") - let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined } + let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined(.roomID("1")) } context.send(viewAction: .acceptInvite) try await deferred.fulfill() @@ -156,6 +156,8 @@ class JoinRoomScreenViewModelTests: XCTestCase { try await deferred.fulfill() } + // MARK: - Helpers + private func setupViewModel(throwing: Bool = false, mode: TestMode = .joined) { ServiceLocator.shared.settings.knockingEnabled = true @@ -199,3 +201,21 @@ class JoinRoomScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController) } } + +extension JoinRoomScreenViewModelAction: @retroactive Equatable { + // A close enough approximation for tests. + public static func == (lhs: JoinRoomScreenViewModelAction, rhs: JoinRoomScreenViewModelAction) -> Bool { + switch (lhs, rhs) { + case (.joined(.roomID(let lhsRoomID)), .joined(.roomID(let rhsRoomID))): + lhsRoomID == rhsRoomID + case (.joined(.space(let lhsSpace)), .joined(.space(let rhsSpace))): + lhsSpace.spaceRoomProxy.id == rhsSpace.spaceRoomProxy.id + case (.dismiss, .dismiss): + true + case (.presentDeclineAndBlock(let lhsUserID), .presentDeclineAndBlock(let rhsUserID)): + lhsUserID == rhsUserID + default: + false + } + } +} diff --git a/UnitTests/Sources/SpaceListScreenViewModelTests.swift b/UnitTests/Sources/SpaceListScreenViewModelTests.swift index 1632ba217..d7bcf9863 100644 --- a/UnitTests/Sources/SpaceListScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceListScreenViewModelTests.swift @@ -72,7 +72,10 @@ class SpaceListScreenViewModelTests: XCTestCase { ]) spaceServiceProxy = SpaceServiceProxyMock(.init()) spaceServiceProxy.joinedSpacesPublisher = joinedSpacesSubject.asCurrentValuePublisher() - spaceServiceProxy.spaceRoomListForClosure = { .success(SpaceRoomListProxyMock(.init(spaceRoomProxy: $0))) } + spaceServiceProxy.spaceRoomListSpaceIDClosure = { [joinedSpacesSubject] spaceID in + guard let spaceRoomProxy = joinedSpacesSubject?.value.first(where: { $0.id == spaceID }) else { return .failure(.missingSpace) } + return .success(SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy))) + } clientProxy.spaceService = spaceServiceProxy viewModel = SpaceListScreenViewModel(userSession: userSession, diff --git a/UnitTests/Sources/SpaceScreenViewModelTests.swift b/UnitTests/Sources/SpaceScreenViewModelTests.swift index 357a6845f..368db5d48 100644 --- a/UnitTests/Sources/SpaceScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceScreenViewModelTests.swift @@ -93,7 +93,7 @@ class SpaceScreenViewModelTests: XCTestCase { func testSelectingSpace() async throws { setupViewModel() - let selectedSpace = try XCTUnwrap(mockSpaceRooms.first { $0.isSpace }, "There should be a space to select.") + let selectedSpace = try XCTUnwrap(mockSpaceRooms.first { $0.isSpace && $0.state == .joined }, "There should be a space to select.") let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true } viewModel.context.send(viewAction: .spaceAction(.select(selectedSpace))) let action = try await deferred.fulfill() @@ -106,6 +106,22 @@ class SpaceScreenViewModelTests: XCTestCase { } } + func testSelectingUnjoinedSpace() async throws { + setupViewModel() + + let selectedSpace = try XCTUnwrap(mockSpaceRooms.first { $0.isSpace && $0.state != .joined }, "There should be a space to select.") + let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true } + viewModel.context.send(viewAction: .spaceAction(.select(selectedSpace))) + let action = try await deferred.fulfill() + + switch action { + case .selectUnjoinedSpace(let spaceRoomProxy) where spaceRoomProxy.id == selectedSpace.id: + break + default: + XCTFail("The action should select the space.") + } + } + func testSelectingRoom() async throws { setupViewModel() @@ -125,7 +141,7 @@ class SpaceScreenViewModelTests: XCTestCase { func testJoiningSpace() async throws { setupViewModel() - let selectedSpace = try XCTUnwrap(mockSpaceRooms.first { $0.isSpace }, "There should be a space to select.") + let selectedSpace = try XCTUnwrap(mockSpaceRooms.first { $0.isSpace && $0.state != .joined }, "There should be a space to select.") let expectation = XCTestExpectation(description: "Join room") clientProxy.joinRoomViaClosure = { _, _ in @@ -184,7 +200,10 @@ class SpaceScreenViewModelTests: XCTestCase { paginationResponses: paginationResponses)) let spaceServiceProxy = SpaceServiceProxyMock(.init()) - spaceServiceProxy.spaceRoomListForClosure = { .success(SpaceRoomListProxyMock(.init(spaceRoomProxy: $0))) } + spaceServiceProxy.spaceRoomListSpaceIDClosure = { [mockSpaceRooms] spaceID in + guard let spaceRoomProxy = mockSpaceRooms.first(where: { $0.id == spaceID }) else { return .failure(.missingSpace) } + return .success(SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy))) + } clientProxy = ClientProxyMock(.init())