From 89c9e3112fdd11c08868479a89912336a9a5c3c8 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:43:48 +0100 Subject: [PATCH] Accept space invites from a home screen cell. (#4554) * Push the SpaceScreen after accepting an invite from the room list. * Add a test for accepting a space invite. --- .../ChatsFlowCoordinator.swift | 75 ++++++++-- .../ChatsFlowCoordinatorStateMachine.swift | 128 ++++++++++-------- .../Sources/Mocks/InvitedRoomProxyMock.swift | 3 +- .../Mocks/RoomSummaryProviderMock.swift | 45 ++++++ .../HomeScreen/HomeScreenCoordinator.swift | 7 +- .../Screens/HomeScreen/HomeScreenModels.swift | 3 +- .../HomeScreen/HomeScreenViewModel.swift | 28 +++- .../Sources/ChatsFlowCoordinatorTests.swift | 62 ++++----- .../Sources/HomeScreenViewModelTests.swift | 97 +++++++++++-- .../UserSessionFlowCoordinatorTests.swift | 14 +- 10 files changed, 339 insertions(+), 123 deletions(-) diff --git a/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinator.swift index 5ca38e15e..674fce74e 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinator.swift @@ -29,6 +29,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { // periphery:ignore - retaining purpose private var roomFlowCoordinator: RoomFlowCoordinator? + // periphery:ignore - retaining purpose + private var spaceFlowCoordinator: SpaceFlowCoordinator? // periphery:ignore - retaining purpose private var bugReportFlowCoordinator: BugReportFlowCoordinator? @@ -117,7 +119,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { } case .roomDetails(let roomID): - if stateMachine.state.roomListSelectedRoomID == roomID { + if case .room(roomID) = stateMachine.state.detailState { roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated) } else { stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .roomDetails), userInfo: .init(animated: animated)) @@ -154,7 +156,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { stateMachine.processEvent(.showShareExtensionRoomList(sharePayload: payload), userInfo: .init(animated: animated)) } case .transferOwnership(let roomID): - if stateMachine.state.roomListSelectedRoomID == roomID { + if case .room(roomID) = stateMachine.state.detailState { roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated) } else { stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .transferOwnership)) @@ -178,12 +180,15 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { private func setupStateMachine() { stateMachine.addTransitionHandler { [weak self] context in guard let self else { return } - let animated = (context.userInfo as? ChatsFlowCoordinatorStateMachine.EventUserInfo)?.animated ?? true + + let userInfo = context.userInfo as? ChatsFlowCoordinatorStateMachine.EventUserInfo + let animated = userInfo?.animated ?? true + switch (context.fromState, context.event, context.toState) { case (.initial, .start, .roomList): presentHomeScreen() - case(.roomList(let roomListSelectedRoomID), .selectRoom(let roomID, let via, let entryPoint), .roomList): - if roomListSelectedRoomID == roomID, + case(.roomList(let detailState), .selectRoom(let roomID, let via, let entryPoint), .roomList): + if case .room(roomID) = detailState, !entryPoint.isEventID, // Don't reuse the existing room so the live timeline is hidden while the detached timeline is loading. let roomFlowCoordinator { let route: AppRoute = switch entryPoint { @@ -195,11 +200,20 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { } roomFlowCoordinator.handleAppRoute(route, animated: animated) } else { + if case .space = detailState { + dismissRoomFlow(animated: animated) + } startRoomFlow(roomID: roomID, via: via, entryPoint: entryPoint, animated: animated) } actionsSubject.send(.hideCallScreenOverlay) // Turn any active call into a PiP so that navigation from a notification is visible to the user. case(.roomList, .deselectRoom, .roomList): dismissRoomFlow(animated: animated) + + case(.roomList, .startSpaceFlow, .roomList): + guard let spaceRoomListProxy = userInfo?.spaceRoomListProxy else { fatalError("A space room list proxy is required.") } + startSpaceFlow(spaceRoomListProxy: spaceRoomListProxy, animated: animated) + case (.roomList, .finishedSpaceFlow, .roomList): + dismissSpaceFlow(animated: animated) case (.roomList, .feedbackScreen, .feedbackScreen): bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .sheet(sidebarNavigationStackCoordinator), @@ -264,8 +278,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { stateMachine.addTransitionHandler { [weak self] context in switch context.toState { - case .roomList(let roomListSelectedRoomID): - self?.selectedRoomSubject.send(roomListSelectedRoomID) + case .roomList(detailState: .room(let detailStateRoomID)): + self?.selectedRoomSubject.send(detailStateRoomID) default: break } @@ -351,9 +365,11 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { handleAppRoute(.roomDetails(roomID: roomID), animated: true) case .presentReportRoom(let roomID): stateMachine.processEvent(.presentReportRoomScreen(roomID: roomID)) + case .presentSpace(let spaceRoomListProxy): + stateMachine.processEvent(.startSpaceFlow, userInfo: .init(animated: true, spaceRoomListProxy: spaceRoomListProxy)) case .roomLeft(let roomID): - if case .roomList(roomListSelectedRoomID: let roomListSelectedRoomID) = stateMachine.state, - roomListSelectedRoomID == roomID { + if case .roomList(detailState: .room(let detailStateRoomID)) = stateMachine.state, + detailStateRoomID == roomID { clearRoute(animated: true) } case .presentSettingsScreen: @@ -398,8 +414,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { switch action { case .dismiss(let shouldLeaveRoom): if shouldLeaveRoom, - case .roomList(let roomListSelectedRoomID) = stateMachine.state, - roomListSelectedRoomID == roomID { + case .roomList(detailState: .room(let detailStateRoomID)) = stateMachine.state, + detailStateRoomID == roomID { clearRoute(animated: true) } navigationSplitCoordinator.setSheetCoordinator(nil) @@ -491,6 +507,43 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { roomFlowCoordinator = nil } + // MARK: Space Flow + + private func startSpaceFlow(spaceRoomListProxy: SpaceRoomListProxyProtocol, animated: Bool) { + let coordinator = SpaceFlowCoordinator(entryPoint: .space(spaceRoomListProxy), + spaceServiceProxy: userSession.clientProxy.spaceService, + isChildFlow: false, + navigationStackCoordinator: detailNavigationStackCoordinator, + flowParameters: flowParameters) + coordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + switch action { + case .presentCallScreen(let roomProxy): + actionsSubject.send(.showCallScreen(roomProxy: roomProxy)) + case .verifyUser(let userID): + actionsSubject.send(.sessionVerification(.userInitiator(userID: userID))) + case .finished: + stateMachine.processEvent(.finishedSpaceFlow) + } + } + .store(in: &cancellables) + + spaceFlowCoordinator = coordinator + + if navigationSplitCoordinator.detailCoordinator !== detailNavigationStackCoordinator { + navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator, animated: animated) + } + + coordinator.start() + } + + private func dismissSpaceFlow(animated: Bool) { + // Based on dismissRoomFlow, past me was very insistent that this must happen after the flow has tidied the stack 😅. + navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated) + roomFlowCoordinator = nil + } + // MARK: Start Chat private func presentStartChat(animated: Bool) { diff --git a/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinatorStateMachine.swift index d0ff6e3c0..2f8cfcbe3 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinatorStateMachine.swift @@ -10,63 +10,69 @@ import Foundation import SwiftState class ChatsFlowCoordinatorStateMachine { + enum DetailState: Hashable { + case room(roomID: String) + case space + } + /// States the AppCoordinator can find itself in enum State: StateType { /// The initial state, used before the coordinator starts case initial /// Showing the home screen. The `roomListSelectedRoomID` represents the timeline shown on the detail panel (if any) - case roomList(roomListSelectedRoomID: String?) + case roomList(detailState: DetailState?) /// Showing the feedback screen. - case feedbackScreen(roomListSelectedRoomID: String?) + case feedbackScreen(detailState: DetailState?) /// Showing the recovery key screen. - case recoveryKeyScreen(roomListSelectedRoomID: String?) + case recoveryKeyScreen(detailState: DetailState?) /// Showing the encryption reset flow. - case encryptionResetFlow(roomListSelectedRoomID: String?) + case encryptionResetFlow(detailState: DetailState?) /// Showing the start chat screen - case startChatScreen(roomListSelectedRoomID: String?) + case startChatScreen(detailState: DetailState?) /// Showing the logout flows - case logoutConfirmationScreen(roomListSelectedRoomID: String?) + case logoutConfirmationScreen(detailState: DetailState?) /// Showing Room Directory Search screen - case roomDirectorySearchScreen(roomListSelectedRoomID: String?) + case roomDirectorySearchScreen(detailState: DetailState?) /// Showing the user profile screen. This screen clears the navigation. case userProfileScreen /// Showing the report room screen, for the given room identrifier - case reportRoomScreen(roomListSelectedRoomID: String?) + case reportRoomScreen(detailState: DetailState?) case shareExtensionRoomList(sharePayload: ShareExtensionPayload) - case declineAndBlockUserScreen(roomListSelectedRoomID: String?) + case declineAndBlockUserScreen(detailState: DetailState?) /// The selected room ID from the state if available. - var roomListSelectedRoomID: String? { + var detailState: DetailState? { switch self { case .initial, .userProfileScreen, .shareExtensionRoomList: nil - case .roomList(let roomListSelectedRoomID), - .feedbackScreen(let roomListSelectedRoomID), - .recoveryKeyScreen(let roomListSelectedRoomID), - .encryptionResetFlow(let roomListSelectedRoomID), - .startChatScreen(let roomListSelectedRoomID), - .logoutConfirmationScreen(let roomListSelectedRoomID), - .roomDirectorySearchScreen(let roomListSelectedRoomID), - .reportRoomScreen(let roomListSelectedRoomID), - .declineAndBlockUserScreen(let roomListSelectedRoomID): - roomListSelectedRoomID + case .roomList(let detailState), + .feedbackScreen(let detailState), + .recoveryKeyScreen(let detailState), + .encryptionResetFlow(let detailState), + .startChatScreen(let detailState), + .logoutConfirmationScreen(let detailState), + .roomDirectorySearchScreen(let detailState), + .reportRoomScreen(let detailState), + .declineAndBlockUserScreen(let detailState): + detailState } } } struct EventUserInfo { let animated: Bool + var spaceRoomListProxy: SpaceRoomListProxyProtocol? } /// Events that can be triggered on the AppCoordinator state machine @@ -82,6 +88,13 @@ class ChatsFlowCoordinatorStateMachine { /// The room screen has been dismissed case deselectRoom + /// Request presentation of a space. + /// + /// The space's `RoomListProxyProtocol` must be provided in the `EventUserInfo`. + case startSpaceFlow + /// The space has been dismissed. + case finishedSpaceFlow + /// Request presentation of the feedback screen case feedbackScreen /// The feedback screen has been dismissed @@ -134,59 +147,64 @@ class ChatsFlowCoordinatorStateMachine { } private func configure() { - stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(roomListSelectedRoomID: nil)]) + stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(detailState: nil)]) stateMachine.addRouteMapping { event, fromState, _ in switch (fromState, event) { case (.roomList, .selectRoom(let roomID, _, _)): - return .roomList(roomListSelectedRoomID: roomID) + return .roomList(detailState: .room(roomID: roomID)) case (.roomList, .deselectRoom): - return .roomList(roomListSelectedRoomID: nil) + return .roomList(detailState: nil) + + case (.roomList, .startSpaceFlow): + return .roomList(detailState: .space) + case (.roomList, .finishedSpaceFlow): + return .roomList(detailState: nil) - case (.roomList(let roomListSelectedRoomID), .feedbackScreen): - return .feedbackScreen(roomListSelectedRoomID: roomListSelectedRoomID) - case (.feedbackScreen(let roomListSelectedRoomID), .dismissedFeedbackScreen): - return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) + case (.roomList(let detailState), .feedbackScreen): + return .feedbackScreen(detailState: detailState) + case (.feedbackScreen(let detailState), .dismissedFeedbackScreen): + return .roomList(detailState: detailState) - case (.roomList(let roomListSelectedRoomID), .showRecoveryKeyScreen): - return .recoveryKeyScreen(roomListSelectedRoomID: roomListSelectedRoomID) - case (.recoveryKeyScreen(let roomListSelectedRoomID), .dismissedRecoveryKeyScreen): - return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) + case (.roomList(let detailState), .showRecoveryKeyScreen): + return .recoveryKeyScreen(detailState: detailState) + case (.recoveryKeyScreen(let detailState), .dismissedRecoveryKeyScreen): + return .roomList(detailState: detailState) - case (.roomList(let roomListSelectedRoomID), .startEncryptionResetFlow): - return .encryptionResetFlow(roomListSelectedRoomID: roomListSelectedRoomID) - case (.encryptionResetFlow(let roomListSelectedRoomID), .finishedEncryptionResetFlow): - return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) + case (.roomList(let detailState), .startEncryptionResetFlow): + return .encryptionResetFlow(detailState: detailState) + case (.encryptionResetFlow(let detailState), .finishedEncryptionResetFlow): + return .roomList(detailState: detailState) - case (.roomList(let roomListSelectedRoomID), .showStartChatScreen): - return .startChatScreen(roomListSelectedRoomID: roomListSelectedRoomID) - case (.startChatScreen(let roomListSelectedRoomID), .dismissedStartChatScreen): - return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) + case (.roomList(let detailState), .showStartChatScreen): + return .startChatScreen(detailState: detailState) + case (.startChatScreen(let detailState), .dismissedStartChatScreen): + return .roomList(detailState: detailState) - case (.roomList(let roomListSelectedRoomID), .showRoomDirectorySearchScreen): - return .roomDirectorySearchScreen(roomListSelectedRoomID: roomListSelectedRoomID) - case (.roomDirectorySearchScreen(let roomListSelectedRoomID), .dismissedRoomDirectorySearchScreen): - return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) + case (.roomList(let detailState), .showRoomDirectorySearchScreen): + return .roomDirectorySearchScreen(detailState: detailState) + case (.roomDirectorySearchScreen(let detailState), .dismissedRoomDirectorySearchScreen): + return .roomList(detailState: detailState) case (_, .showUserProfileScreen): return .userProfileScreen case (.userProfileScreen, .dismissedUserProfileScreen): - return .roomList(roomListSelectedRoomID: nil) + return .roomList(detailState: nil) case (.roomList, .showShareExtensionRoomList(let sharePayload)): return .shareExtensionRoomList(sharePayload: sharePayload) case (.shareExtensionRoomList, .dismissedShareExtensionRoomList): - return .roomList(roomListSelectedRoomID: nil) + return .roomList(detailState: nil) - case (.roomList(let roomListSelectedRoomID), .presentReportRoomScreen): - return .reportRoomScreen(roomListSelectedRoomID: roomListSelectedRoomID) - case (.reportRoomScreen(let roomListSelectedRoomID), .dismissedReportRoomScreen): - return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) + case (.roomList(let detailState), .presentReportRoomScreen): + return .reportRoomScreen(detailState: detailState) + case (.reportRoomScreen(let detailState), .dismissedReportRoomScreen): + return .roomList(detailState: detailState) - case(.roomList(let roomListSelectedRoomID), .presentDeclineAndBlockScreen): - return .declineAndBlockUserScreen(roomListSelectedRoomID: roomListSelectedRoomID) - case (.declineAndBlockUserScreen(let roomListSelectedRoomID), .dismissedDeclineAndBlockScreen): - return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) + case(.roomList(let detailState), .presentDeclineAndBlockScreen): + return .declineAndBlockUserScreen(detailState: detailState) + case (.declineAndBlockUserScreen(let detailState), .dismissedDeclineAndBlockScreen): + return .roomList(detailState: detailState) default: return nil @@ -221,8 +239,8 @@ class ChatsFlowCoordinatorStateMachine { /// Flag indicating the machine is displaying room screen with given room identifier func isDisplayingRoomScreen(withRoomID roomID: String) -> Bool { switch stateMachine.state { - case .roomList(let roomListSelectedRoomID): - return roomID == roomListSelectedRoomID + case .roomList(detailState: .room(let detailStateRoomID)): + return roomID == detailStateRoomID default: return false } diff --git a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift index c4e7abb87..c6fb1c497 100644 --- a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift @@ -14,6 +14,7 @@ struct InvitedRoomProxyMockConfiguration { var id = UUID().uuidString var name: String? var avatarURL: URL? + var isSpace = false var members: [RoomMemberProxyMock] = .allMembers var inviter: RoomMemberProxyMock = .mockAlice } @@ -44,7 +45,7 @@ extension RoomInfoProxyMock { avatarURL = configuration.avatarURL isDirect = false - isSpace = false + isSpace = configuration.isSpace successor = nil isFavourite = false canonicalAlias = nil diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index 3bdb21e50..e22e4e28d 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -341,4 +341,49 @@ extension Array where Element == RoomSummary { isFavourite: false, isTombstoned: false) ] + + static let mockSpaceInvites: [Element] = [ + RoomSummary(room: RoomSDKMock(), + id: "someAwesomeSpaceID1", + joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), + name: "First space", + isDirect: false, + isSpace: true, + avatarURL: .mockMXCAvatar, + heroes: [], + activeMembersCount: 0, + lastMessage: nil, + lastMessageDate: nil, + unreadMessagesCount: 0, + unreadMentionsCount: 0, + unreadNotificationsCount: 0, + notificationMode: nil, + canonicalAlias: "#footest:somewhere.org", + alternativeAliases: [], + hasOngoingCall: false, + isMarkedUnread: false, + isFavourite: false, + isTombstoned: false), + RoomSummary(room: RoomSDKMock(), + id: "someAwesomeSpaceID2", + joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), + name: "Second space", + isDirect: false, + isSpace: true, + avatarURL: nil, + heroes: [], + activeMembersCount: 0, + lastMessage: nil, + lastMessageDate: nil, + unreadMessagesCount: 0, + unreadMentionsCount: 0, + unreadNotificationsCount: 0, + notificationMode: nil, + canonicalAlias: nil, + alternativeAliases: [], + hasOngoingCall: false, + isMarkedUnread: false, + isFavourite: false, + isTombstoned: false) + ] } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index f163c88c0..4750bfa09 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -23,6 +23,7 @@ enum HomeScreenCoordinatorAction { case presentRoomDetails(roomIdentifier: String) case presentReportRoom(roomIdentifier: String) case presentDeclineAndBlock(userID: String, roomID: String) + case presentSpace(SpaceRoomListProxyProtocol) case roomLeft(roomIdentifier: String) case transferOwnership(roomIdentifier: String) case presentSettingsScreen @@ -67,6 +68,10 @@ final class HomeScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier)) case .presentReportRoom(let roomIdentifier): actionsSubject.send(.presentReportRoom(roomIdentifier: roomIdentifier)) + case .presentDeclineAndBlock(let userID, let roomID): + actionsSubject.send(.presentDeclineAndBlock(userID: userID, roomID: roomID)) + case .presentSpace(let spaceRoomListProxy): + actionsSubject.send(.presentSpace(spaceRoomListProxy)) case .roomLeft(roomIdentifier: let roomIdentifier): actionsSubject.send(.roomLeft(roomIdentifier: roomIdentifier)) case .presentFeedbackScreen: @@ -85,8 +90,6 @@ final class HomeScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentGlobalSearch) case .logout: actionsSubject.send(.logout) - case .presentDeclineAndBlock(let userID, let roomID): - actionsSubject.send(.presentDeclineAndBlock(userID: userID, roomID: roomID)) case .transferOwnership(let roomIdentifier): actionsSubject.send(.transferOwnership(roomIdentifier: roomIdentifier)) } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index d304aa2eb..7b1b6dafd 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -9,11 +9,12 @@ import Combine import Foundation import UIKit -enum HomeScreenViewModelAction: Equatable { +enum HomeScreenViewModelAction { case presentRoom(roomIdentifier: String) case presentRoomDetails(roomIdentifier: String) case presentReportRoom(roomIdentifier: String) case presentDeclineAndBlock(userID: String, roomID: String) + case presentSpace(SpaceRoomListProxyProtocol) case roomLeft(roomIdentifier: String) case transferOwnership(roomIdentifier: String) case presentSecureBackupSettings diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index da1813e15..177bcc693 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -414,11 +414,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol switch await userSession.clientProxy.joinRoom(roomID, via: []) { case .success: - actionsSubject.send(.presentRoom(roomIdentifier: roomID)) - analyticsService.trackJoinedRoom(isDM: roomProxy.info.isDirect, - isSpace: roomProxy.info.isSpace, - activeMemberCount: UInt(roomProxy.info.activeMembersCount)) - appSettings.seenInvites.remove(roomID) + await finishAcceptInvite(roomProxy: roomProxy) case .failure(let error): switch error { case .invalidInvite: @@ -429,6 +425,28 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } } + private func finishAcceptInvite(roomProxy: InvitedRoomProxyProtocol) async { + if roomProxy.info.isSpace { + let spaceService = userSession.clientProxy.spaceService + + switch await spaceService.spaceRoomList(spaceID: roomProxy.id, parent: nil) { + case .success(let spaceRoomListProxy): + actionsSubject.send(.presentSpace(spaceRoomListProxy)) + case .failure(let error): + MXLog.error("Failed to get the space room list after accepting invite: \(error)") + displayError() + return + } + } else { + actionsSubject.send(.presentRoom(roomIdentifier: roomProxy.id)) + } + + analyticsService.trackJoinedRoom(isDM: roomProxy.info.isDirect, + isSpace: roomProxy.info.isSpace, + activeMemberCount: UInt(roomProxy.info.activeMembersCount)) + appSettings.seenInvites.remove(roomProxy.id) + } + private func showDeclineInviteConfirmationAlert(roomID: String) async { guard let room = state.rooms.first(where: { $0.id == roomID }) else { displayError() diff --git a/UnitTests/Sources/ChatsFlowCoordinatorTests.swift b/UnitTests/Sources/ChatsFlowCoordinatorTests.swift index 4a0e6b40a..df38d72f6 100644 --- a/UnitTests/Sources/ChatsFlowCoordinatorTests.swift +++ b/UnitTests/Sources/ChatsFlowCoordinatorTests.swift @@ -50,29 +50,29 @@ class ChatsFlowCoordinatorTests: XCTestCase { navigationSplitCoordinator: splitCoordinator, flowParameters: flowParameters) - let deferred = deferFulfillment(stateMachineFactory.chatsFlowStatePublisher) { $0 == .roomList(roomListSelectedRoomID: nil) } + let deferred = deferFulfillment(stateMachineFactory.chatsFlowStatePublisher) { $0 == .roomList(detailState: nil) } chatsFlowCoordinator.start() try await deferred.fulfill() } func testRoomPresentation() async throws { - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(detailState: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(roomListSelectedRoomID: "2")) + try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(detailState: .room(roomID: "2"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(detailState: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) @@ -82,25 +82,25 @@ class ChatsFlowCoordinatorTests: XCTestCase { func testRoomAliasPresentation() async throws { clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "1", servers: [])) - try await process(route: .roomAlias("#alias:matrix.org"), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .roomAlias("#alias:matrix.org"), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(detailState: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "2", servers: [])) - try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(roomListSelectedRoomID: "2")) + try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(detailState: .room(roomID: "2"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(detailState: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) @@ -108,27 +108,27 @@ class ChatsFlowCoordinatorTests: XCTestCase { } func testRoomDetailsPresentation() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(detailState: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) } func testStackUnwinding() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(roomListSelectedRoomID: "2")) + try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(detailState: .room(roomID: "2"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) } func testNoOp() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) @@ -141,17 +141,17 @@ class ChatsFlowCoordinatorTests: XCTestCase { } func testSwitchToDifferentDetails() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomDetails(roomID: "2"), expectedState: .roomList(roomListSelectedRoomID: "2")) + try await process(route: .roomDetails(roomID: "2"), expectedState: .roomList(detailState: .room(roomID: "2"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) } func testPushDetails() async throws { - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) @@ -166,17 +166,17 @@ class ChatsFlowCoordinatorTests: XCTestCase { } func testReplaceDetailsWithTimeline() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) } func testUserProfileClearsStack() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) XCTAssertNil(splitCoordinator?.sheetCoordinator) @@ -191,7 +191,7 @@ class ChatsFlowCoordinatorTests: XCTestCase { } func testRoomClearsStack() async throws { - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -203,7 +203,7 @@ class ChatsFlowCoordinatorTests: XCTestCase { XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .room(roomID: "3", via: []), expectedState: .roomList(roomListSelectedRoomID: "3")) + try await process(route: .room(roomID: "3", via: []), expectedState: .roomList(detailState: .room(roomID: "3"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -211,7 +211,7 @@ class ChatsFlowCoordinatorTests: XCTestCase { func testEventRoutes() async throws { // A regular event route should set its room as the root of the stack and focus on the event. - try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -229,7 +229,7 @@ class ChatsFlowCoordinatorTests: XCTestCase { XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "2") // A subsequent regular event route should clear the stack and set the new room as the root of the stack. - try await process(route: .event(eventID: "3", roomID: "3", via: []), expectedState: .roomList(roomListSelectedRoomID: "3")) + try await process(route: .event(eventID: "3", roomID: "3", via: []), expectedState: .roomList(detailState: .room(roomID: "3"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -237,7 +237,7 @@ class ChatsFlowCoordinatorTests: XCTestCase { XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "3") // A regular event route for the same room should set a new instance of the room as the root of the stack. - try await process(route: .event(eventID: "4", roomID: "3", via: []), expectedState: .roomList(roomListSelectedRoomID: "3")) + try await process(route: .event(eventID: "4", roomID: "3", via: []), expectedState: .roomList(detailState: .room(roomID: "3"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -247,24 +247,24 @@ class ChatsFlowCoordinatorTests: XCTestCase { } func testShareMediaRouteWithRoom() async throws { - try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: "2", mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)]) try await process(route: .share(sharePayload), - expectedState: .roomList(roomListSelectedRoomID: "2")) + expectedState: .roomList(detailState: .room(roomID: "2"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) } func testShareTextRouteWithRoom() async throws { - try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) let sharePayload: ShareExtensionPayload = .text(roomID: "2", text: "Important text") try await process(route: .share(sharePayload), - expectedState: .roomList(roomListSelectedRoomID: "2")) + expectedState: .roomList(detailState: .room(roomID: "2"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNil(splitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.") diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 1565e0f66..5d1aa0c2d 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -265,7 +265,7 @@ class HomeScreenViewModelTests: XCTestCase { } func testInviteUnreadBadge() async throws { - setupViewModel(withInvites: true) + setupViewModel(invites: .rooms) var invites = context.viewState.rooms.invites XCTAssertEqual(invites.count, 2) @@ -288,7 +288,7 @@ class HomeScreenViewModelTests: XCTestCase { } func testAcceptInvite() async throws { - setupViewModel(withInvites: true) + setupViewModel(invites: .rooms) let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) appSettings.seenInvites = Set(invitedRoomIDs) @@ -302,8 +302,25 @@ class HomeScreenViewModelTests: XCTestCase { XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.") } + func testAcceptSpaceInvite() async throws { + setupViewModel(invites: .spaces) + + let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) + appSettings.seenInvites = Set(invitedRoomIDs) + XCTAssertEqual(invitedRoomIDs.count, 2) + + let deferred = deferFulfillment(viewModel.actions) { + $0 == .presentSpace(SpaceRoomListProxyMock(.init(spaceRoomProxy: SpaceRoomProxyMock(.init(id: invitedRoomIDs[0], isSpace: true))))) + } + context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0])) + try await deferred.fulfill() + + XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]]) + XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.") + } + func testDeclineInvite() async throws { - setupViewModel(withInvites: true) + setupViewModel(invites: .rooms) let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) appSettings.seenInvites = Set(invitedRoomIDs) XCTAssertEqual(invitedRoomIDs.count, 2) @@ -331,7 +348,7 @@ class HomeScreenViewModelTests: XCTestCase { } func testDeclineAndBlockInvite() async throws { - setupViewModel(withInvites: true) + setupViewModel(invites: .rooms) let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) appSettings.seenInvites = Set(invitedRoomIDs) XCTAssertEqual(invitedRoomIDs.count, 2) @@ -347,20 +364,41 @@ class HomeScreenViewModelTests: XCTestCase { // MARK: - Helpers - private func setupViewModel(securityStatePublisher: CurrentValuePublisher? = nil, withInvites: Bool = false) { + enum InviteType { case rooms, spaces } + + private func setupViewModel(securityStatePublisher: CurrentValuePublisher? = nil, invites: InviteType? = nil) { var rooms: [RoomSummary] = .mockRooms - if withInvites { + + switch invites { + case .rooms: rooms += .mockInvites + case .spaces: + rooms += .mockSpaceInvites + case nil: + break } roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(rooms))) clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", roomSummaryProvider: roomSummaryProvider)) - if withInvites { - clientProxy.joinRoomViaReturnValue = .success(()) - clientProxy.joinRoomAliasReturnValue = .success(()) - clientProxy.roomForIdentifierClosure = { _ in .invited(InvitedRoomProxyMock(.init())) } + + clientProxy.joinRoomViaReturnValue = .success(()) + clientProxy.joinRoomAliasReturnValue = .success(()) + + switch invites { + case .rooms: + clientProxy.roomForIdentifierClosure = { roomID in .invited(InvitedRoomProxyMock(.init(id: roomID))) } + case .spaces: + clientProxy.roomForIdentifierClosure = { spaceID in .invited(InvitedRoomProxyMock(.init(id: spaceID, isSpace: true))) } + + let spaceServiceProxy = SpaceServiceProxyMock(.init()) + spaceServiceProxy.spaceRoomListSpaceIDParentClosure = { spaceID, _ in + .success(SpaceRoomListProxyMock(.init(spaceRoomProxy: SpaceRoomProxyMock(.init(id: spaceID, isSpace: true))))) + } + clientProxy.underlyingSpaceService = spaceServiceProxy + case nil: + break } let userSession = UserSessionMock(.init(clientProxy: clientProxy)) @@ -390,3 +428,42 @@ private extension [HomeScreenRoom] { } } } + +extension HomeScreenViewModelAction: @retroactive Equatable { + public static func == (lhs: HomeScreenViewModelAction, rhs: HomeScreenViewModelAction) -> Bool { + switch (lhs, rhs) { + case (.presentRoom(let lhsID), .presentRoom(let rhsID)): + lhsID == rhsID + case (.presentRoomDetails(let lhsID), .presentRoomDetails(let rhsID)): + lhsID == rhsID + case (.presentReportRoom(let lhsID), .presentReportRoom(let rhsID)): + lhsID == rhsID + case (.presentDeclineAndBlock(let lhsUserID, let lhsRoomID), .presentDeclineAndBlock(let rhsUserID, let rhsRoomID)): + lhsUserID == rhsUserID && lhsRoomID == rhsRoomID + case (.presentSpace(let lhsSpaceRoomListProxy), .presentSpace(let rhsSpaceRoomListProxy)): + lhsSpaceRoomListProxy.spaceRoomProxy.id == rhsSpaceRoomListProxy.spaceRoomProxy.id + case (.roomLeft(let lhsID), .roomLeft(let rhsID)): + lhsID == rhsID + case (.transferOwnership(let lhsID), .transferOwnership(let rhsID)): + lhsID == rhsID + case (.presentSecureBackupSettings, .presentSecureBackupSettings): + true + case (.presentRecoveryKeyScreen, .presentRecoveryKeyScreen): + true + case (.presentEncryptionResetScreen, .presentEncryptionResetScreen): + true + case (.presentSettingsScreen, .presentSettingsScreen): + true + case (.presentFeedbackScreen, .presentFeedbackScreen): + true + case (.presentStartChatScreen, .presentStartChatScreen): + true + case (.presentGlobalSearch, .presentGlobalSearch): + true + case (.logout, .logout): + true + default: + false + } + } +} diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 512d49f69..448ae67f3 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -76,7 +76,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testRoomPresentation() async throws { - try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) } @@ -86,14 +86,14 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) XCTAssertNil(detailCoordinator) - try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) XCTAssertNil((tabCoordinator?.sheetCoordinator)) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) } func testChildRoomPresentation() async throws { - try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) let detailNavigationStack = try XCTUnwrap(detailNavigationStack, "There must be a navigation stack.") XCTAssertTrue(detailNavigationStack.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) @@ -121,14 +121,14 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testShareMediaRouteWithRoom() async throws { - try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNil(tabCoordinator?.sheetCoordinator) XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator) let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: "2", mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)]) try await process(route: .share(sharePayload), - expectedChatsState: .roomList(roomListSelectedRoomID: "2")) + expectedChatsState: .roomList(detailState: .room(roomID: "2"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNil(tabCoordinator?.sheetCoordinator) @@ -149,14 +149,14 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testShareTextRouteWithRoom() async throws { - try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(roomListSelectedRoomID: "1")) + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNil(tabCoordinator?.sheetCoordinator) XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator) let sharePayload: ShareExtensionPayload = .text(roomID: "2", text: "Important text") try await process(route: .share(sharePayload), - expectedChatsState: .roomList(roomListSelectedRoomID: "2")) + expectedChatsState: .roomList(detailState: .room(roomID: "2"))) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNil(tabCoordinator?.sheetCoordinator)