mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Move the Settings flow from the Chats flow up one level to the UserSession flow. (#4390)
* Add the basis for a state machine to UserSessionFlowCoordinator. * Move the Settings flow from the Chats flow to UserSession flow. It now works from the Spaces tab. * Add a StateMachineFactory and use it to publish the state in the tests.
This commit is contained in:
@@ -519,6 +519,7 @@
|
||||
61941DEE5F3834765770BE01 /* InviteUsersScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F32E0B4B83D2A11EE8D011 /* InviteUsersScreenSelectedItem.swift */; };
|
||||
61A36B9BB2ADE36CEFF5E98C /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */; };
|
||||
62684AECDFC5C7DC989CBD9E /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 7B6BC3219ADD8AA0311D2B86 /* SnapshotTesting */; };
|
||||
627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */; };
|
||||
6298AB0906DDD3525CD78C6B /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = 81DB3AB6CE996AB3954F4F03 /* KZFileWatchers */; };
|
||||
62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */; };
|
||||
62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */; };
|
||||
@@ -1215,6 +1216,7 @@
|
||||
E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */; };
|
||||
E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; };
|
||||
E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; };
|
||||
E8AE3BE5C5D2E6C91751A2A6 /* StateMachineFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC73285743189AE4498A51DD /* StateMachineFactory.swift */; };
|
||||
E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.swift */; };
|
||||
E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3877D3CFAC1D33823359BAF /* HTMLParserStyle.swift */; };
|
||||
E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */; };
|
||||
@@ -1849,6 +1851,7 @@
|
||||
4E7F7A975514E850A834B29F /* PaginationIndicatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
4F5F0662483ED69791D63B16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = et; path = et.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
4FAC770857E525B51E277D8C /* SpaceListScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceListScreenModels.swift; sourceTree = "<group>"; };
|
||||
4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -2300,6 +2303,7 @@
|
||||
AC43313F21511C853D34544E /* SoftLogoutScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerTests.swift; sourceTree = "<group>"; };
|
||||
AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleFlowLayoutTests.swift; sourceTree = "<group>"; };
|
||||
AC73285743189AE4498A51DD /* StateMachineFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateMachineFactory.swift; sourceTree = "<group>"; };
|
||||
AC9104846487244648D32C6D /* AudioPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerProtocol.swift; sourceTree = "<group>"; };
|
||||
ACA11F7F50A4A3887A18CA5A /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
ACD7BD6BEE21264F6677904A /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
@@ -2947,6 +2951,7 @@
|
||||
BDCEF7C3BF6D09F5611CFC8B /* SecureBackup */,
|
||||
82D5AD3EAE3A5C1068A44A88 /* Session */,
|
||||
5329E48968EB951235E83DAE /* SessionVerification */,
|
||||
AA94873C78BBB969CE65EE52 /* StateMachine */,
|
||||
FCDF06BDB123505F0334B4F9 /* Timeline */,
|
||||
E4E42F93A69AE52E6FAE9412 /* Users */,
|
||||
CBBF6127C313A5412E438BC6 /* UserSession */,
|
||||
@@ -4522,6 +4527,7 @@
|
||||
2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */,
|
||||
BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */,
|
||||
71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */,
|
||||
4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */,
|
||||
283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */,
|
||||
AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */,
|
||||
D93C94C30E3135BC9290DE13 /* VoiceMessageRecorderTests.swift */,
|
||||
@@ -5368,6 +5374,14 @@
|
||||
path = DevelopmentAssets;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AA94873C78BBB969CE65EE52 /* StateMachine */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AC73285743189AE4498A51DD /* StateMachineFactory.swift */,
|
||||
);
|
||||
path = StateMachine;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AAFDD509929A0CCF8BCE51EB /* Authentication */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -7187,6 +7201,7 @@
|
||||
A1DF0E1E526A981ED6D5DF44 /* UserIndicatorControllerTests.swift in Sources */,
|
||||
04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */,
|
||||
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */,
|
||||
627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */,
|
||||
81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */,
|
||||
21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */,
|
||||
44BDD670FF9095ACE240A3A2 /* VoiceMessageMediaManagerTests.swift in Sources */,
|
||||
@@ -8023,6 +8038,7 @@
|
||||
C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */,
|
||||
4A85928E27D4C1A548A06EE9 /* StartChatScreenViewModel.swift in Sources */,
|
||||
B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */,
|
||||
E8AE3BE5C5D2E6C91751A2A6 /* StateMachineFactory.swift in Sources */,
|
||||
CB6BCBF28E4B76EA08C2926D /* StateRoomTimelineItem.swift in Sources */,
|
||||
3B277D9538090766DA6C4566 /* StateRoomTimelineView.swift in Sources */,
|
||||
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */,
|
||||
|
||||
@@ -641,6 +641,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
}
|
||||
|
||||
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
|
||||
isNewLogin: isNewLogin,
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
appLockService: appLockFlowCoordinator.appLockService,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
@@ -651,7 +652,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
appHooks: appHooks,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
notificationManager: notificationManager,
|
||||
isNewLogin: isNewLogin)
|
||||
stateMachineFactory: StateMachineFactory())
|
||||
|
||||
userSessionFlowCoordinator.actionsPublisher
|
||||
.sink { [weak self] action in
|
||||
|
||||
@@ -12,11 +12,10 @@ import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
enum ChatsFlowCoordinatorAction {
|
||||
case logout
|
||||
case showSettings
|
||||
case showChatBackupSettings
|
||||
case sessionVerification(SessionVerificationScreenFlow)
|
||||
case clearCache
|
||||
/// Logout and disable App Lock without any confirmation. The user forgot their PIN.
|
||||
case forceLogout
|
||||
case logout
|
||||
}
|
||||
|
||||
class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
@@ -36,8 +35,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private var roomFlowCoordinator: RoomFlowCoordinator?
|
||||
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
|
||||
private let settingsFlowCoordinator: SettingsFlowCoordinator
|
||||
|
||||
// periphery:ignore - retaining purpose
|
||||
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
|
||||
|
||||
@@ -59,10 +56,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// For testing purposes.
|
||||
var statePublisher: AnyPublisher<ChatsFlowCoordinatorStateMachine.State, Never> { stateMachine.statePublisher }
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
isNewLogin: Bool,
|
||||
navigationSplitCoordinator: NavigationSplitCoordinator,
|
||||
appLockService: AppLockServiceProtocol,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
@@ -73,8 +68,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
appHooks: AppHooks,
|
||||
analytics: AnalyticsService,
|
||||
notificationManager: NotificationManagerProtocol,
|
||||
isNewLogin: Bool) {
|
||||
stateMachine = ChatsFlowCoordinatorStateMachine()
|
||||
stateMachineFactory: StateMachineFactoryProtocol) {
|
||||
stateMachine = stateMachineFactory.makeChatsFlowStateMachine()
|
||||
self.userSession = userSession
|
||||
self.navigationSplitCoordinator = navigationSplitCoordinator
|
||||
self.bugReportService = bugReportService
|
||||
@@ -88,22 +83,9 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
settingsFlowCoordinator = SettingsFlowCoordinator(parameters: .init(userSession: userSession,
|
||||
windowManager: appMediator.windowManager,
|
||||
appLockService: appLockService,
|
||||
bugReportService: bugReportService,
|
||||
notificationSettings: userSession.clientProxy.notificationSettings,
|
||||
secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
appSettings: appSettings,
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: analytics))
|
||||
|
||||
setupStateMachine()
|
||||
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
@@ -189,8 +171,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
Task { await presentCallScreen(roomID: roomID) }
|
||||
case .genericCallLink(let url):
|
||||
presentCallScreen(genericCallLink: url)
|
||||
case .settings, .chatBackupSettings:
|
||||
settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
case .share(let payload):
|
||||
if let roomID = payload.roomID {
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID,
|
||||
@@ -200,14 +180,14 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
} else {
|
||||
stateMachine.processEvent(.showShareExtensionRoomList(sharePayload: payload), userInfo: .init(animated: animated))
|
||||
}
|
||||
case .accountProvisioningLink:
|
||||
break // We always ignore this flow when logged in.
|
||||
case .transferOwnership(let roomID):
|
||||
if stateMachine.state.roomListSelectedRoomID == roomID {
|
||||
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
||||
} else {
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .transferOwnership))
|
||||
}
|
||||
case .accountProvisioningLink, .settings, .chatBackupSettings:
|
||||
break // These routes cannot be handled.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,11 +227,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
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, .showSettingsScreen, .settingsScreen):
|
||||
break
|
||||
case (.settingsScreen, .dismissedSettingsScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.roomList, .feedbackScreen, .feedbackScreen):
|
||||
bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .sheet(sidebarNavigationStackCoordinator),
|
||||
@@ -333,24 +308,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
settingsFlowCoordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentedSettings:
|
||||
stateMachine.processEvent(.showSettingsScreen)
|
||||
case .dismissedSettings:
|
||||
stateMachine.processEvent(.dismissedSettingsScreen)
|
||||
case .runLogoutFlow:
|
||||
actionsSubject.send(.logout)
|
||||
case .clearCache:
|
||||
actionsSubject.send(.clearCache)
|
||||
case .forceLogout:
|
||||
actionsSubject.send(.forceLogout)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
userSession.clientProxy.actionsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] action in
|
||||
@@ -439,11 +396,11 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
clearRoute(animated: true)
|
||||
}
|
||||
case .presentSettingsScreen:
|
||||
settingsFlowCoordinator.handleAppRoute(.settings, animated: true)
|
||||
actionsSubject.send(.showSettings)
|
||||
case .presentFeedbackScreen:
|
||||
stateMachine.processEvent(.feedbackScreen)
|
||||
case .presentSecureBackupSettings:
|
||||
settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
|
||||
actionsSubject.send(.showChatBackupSettings)
|
||||
case .presentRecoveryKeyScreen:
|
||||
stateMachine.processEvent(.showRecoveryKeyScreen)
|
||||
case .presentEncryptionResetScreen:
|
||||
|
||||
@@ -21,9 +21,6 @@ class ChatsFlowCoordinatorStateMachine {
|
||||
/// Showing the feedback screen.
|
||||
case feedbackScreen(roomListSelectedRoomID: String?)
|
||||
|
||||
/// Showing the settings screen
|
||||
case settingsScreen(roomListSelectedRoomID: String?)
|
||||
|
||||
/// Showing the recovery key screen.
|
||||
case recoveryKeyScreen(roomListSelectedRoomID: String?)
|
||||
|
||||
@@ -56,7 +53,6 @@ class ChatsFlowCoordinatorStateMachine {
|
||||
nil
|
||||
case .roomList(let roomListSelectedRoomID),
|
||||
.feedbackScreen(let roomListSelectedRoomID),
|
||||
.settingsScreen(let roomListSelectedRoomID),
|
||||
.recoveryKeyScreen(let roomListSelectedRoomID),
|
||||
.encryptionResetFlow(let roomListSelectedRoomID),
|
||||
.startChatScreen(let roomListSelectedRoomID),
|
||||
@@ -86,11 +82,6 @@ class ChatsFlowCoordinatorStateMachine {
|
||||
/// The room screen has been dismissed
|
||||
case deselectRoom
|
||||
|
||||
/// Request presentation of the settings screen
|
||||
case showSettingsScreen
|
||||
/// The settings screen has been dismissed
|
||||
case dismissedSettingsScreen
|
||||
|
||||
/// Request presentation of the feedback screen
|
||||
case feedbackScreen
|
||||
/// The feedback screen has been dismissed
|
||||
@@ -137,11 +128,6 @@ class ChatsFlowCoordinatorStateMachine {
|
||||
stateMachine.state
|
||||
}
|
||||
|
||||
var stateSubject = PassthroughSubject<State, Never>()
|
||||
var statePublisher: AnyPublisher<State, Never> {
|
||||
stateSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
stateMachine = StateMachine(state: .initial)
|
||||
configure()
|
||||
@@ -156,11 +142,6 @@ class ChatsFlowCoordinatorStateMachine {
|
||||
return .roomList(roomListSelectedRoomID: roomID)
|
||||
case (.roomList, .deselectRoom):
|
||||
return .roomList(roomListSelectedRoomID: nil)
|
||||
|
||||
case (.roomList(let roomListSelectedRoomID), .showSettingsScreen):
|
||||
return .settingsScreen(roomListSelectedRoomID: roomListSelectedRoomID)
|
||||
case (.settingsScreen(let roomListSelectedRoomID), .dismissedSettingsScreen):
|
||||
return .roomList(roomListSelectedRoomID: roomListSelectedRoomID)
|
||||
|
||||
case (.roomList(let roomListSelectedRoomID), .feedbackScreen):
|
||||
return .feedbackScreen(roomListSelectedRoomID: roomListSelectedRoomID)
|
||||
@@ -219,10 +200,6 @@ class ChatsFlowCoordinatorStateMachine {
|
||||
MXLog.info("Transitioning from \(context.fromState)` to `\(context.toState)`")
|
||||
}
|
||||
}
|
||||
|
||||
addTransitionHandler { [weak self] context in
|
||||
self?.stateSubject.send(context.toState)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to move the state machine to another state through an event
|
||||
|
||||
@@ -9,10 +9,9 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
enum SettingsFlowCoordinatorAction {
|
||||
case presentedSettings
|
||||
case dismissedSettings
|
||||
case runLogoutFlow
|
||||
case dismiss
|
||||
case clearCache
|
||||
case runLogoutFlow
|
||||
/// Logout without a confirmation. The user forgot their PIN.
|
||||
case forceLogout
|
||||
}
|
||||
@@ -25,17 +24,14 @@ struct SettingsFlowCoordinatorParameters {
|
||||
let notificationSettings: NotificationSettingsProxyProtocol
|
||||
let secureBackupController: SecureBackupControllerProtocol
|
||||
let appSettings: AppSettings
|
||||
let navigationSplitCoordinator: NavigationSplitCoordinator
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let parameters: SettingsFlowCoordinatorParameters
|
||||
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator!
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
|
||||
|
||||
// periphery:ignore - retaining purpose
|
||||
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
|
||||
@@ -44,6 +40,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
// periphery:ignore - retaining purpose
|
||||
private var encryptionSettingsFlowCoordinator: EncryptionSettingsFlowCoordinator?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SettingsFlowCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<SettingsFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
@@ -62,15 +60,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .settings:
|
||||
presentSettingsScreen(animated: animated)
|
||||
case .chatBackupSettings:
|
||||
if navigationStackCoordinator == nil {
|
||||
presentSettingsScreen(animated: animated)
|
||||
}
|
||||
|
||||
// The navigation stack doesn't like it if the root and the push happen
|
||||
// on the same loop run
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
self.startEncryptionSettingsFlow(animated: animated)
|
||||
}
|
||||
startEncryptionSettingsFlow(animated: animated)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -83,8 +73,6 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
// MARK: - Private
|
||||
|
||||
private func presentSettingsScreen(animated: Bool) {
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: parameters.userSession,
|
||||
appSettings: parameters.appSettings,
|
||||
isBugReportServiceEnabled: parameters.bugReportService.isEnabled))
|
||||
@@ -95,14 +83,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
parameters.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
actionsSubject.send(.dismiss)
|
||||
case .logout:
|
||||
parameters.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
// The settings sheet needs to be dismissed before the alert can be shown
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.actionsSubject.send(.runLogoutFlow)
|
||||
}
|
||||
actionsSubject.send(.runLogoutFlow)
|
||||
case .secureBackup:
|
||||
startEncryptionSettingsFlow(animated: true)
|
||||
case .userDetails:
|
||||
@@ -136,15 +119,6 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(settingsScreenCoordinator, animated: animated)
|
||||
|
||||
parameters.navigationSplitCoordinator.setSheetCoordinator(navigationStackCoordinator) { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
navigationStackCoordinator = nil
|
||||
actionsSubject.send(.dismissedSettings)
|
||||
}
|
||||
|
||||
actionsSubject.send(.presentedSettings)
|
||||
}
|
||||
|
||||
private func startEncryptionSettingsFlow(animated: Bool) {
|
||||
@@ -174,13 +148,13 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings))
|
||||
|
||||
navigationStackCoordinator?.push(coordinator)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentAnalyticsScreen() {
|
||||
let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: parameters.appSettings,
|
||||
analytics: parameters.analytics))
|
||||
navigationStackCoordinator?.push(coordinator)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentAppLockSetupFlow() {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Combine
|
||||
import Compound
|
||||
import MatrixRustSDK
|
||||
import SwiftState
|
||||
import SwiftUI
|
||||
|
||||
enum UserSessionFlowCoordinatorAction {
|
||||
@@ -23,8 +24,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let navigationTabCoordinator: NavigationTabCoordinator<HomeTab>
|
||||
private let appLockService: AppLockServiceProtocol
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let appMediator: AppMediatorProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let analytics: AnalyticsService
|
||||
|
||||
private let onboardingFlowCoordinator: OnboardingFlowCoordinator
|
||||
private let onboardingStackCoordinator: NavigationStackCoordinator
|
||||
@@ -33,14 +37,38 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let spaceExplorerFlowCoordinator: SpaceExplorerFlowCoordinator
|
||||
private let spacesTabDetails: NavigationTabCoordinator<HomeTab>.TabDetails
|
||||
|
||||
// periphery:ignore - retaining purpose
|
||||
private var settingsFlowCoordinator: SettingsFlowCoordinator?
|
||||
|
||||
enum State: StateType {
|
||||
/// The state machine hasn't started.
|
||||
case initial
|
||||
/// The root screen for this flow.
|
||||
case tabBar
|
||||
/// Showing the settings screen.
|
||||
case settingsScreen
|
||||
}
|
||||
|
||||
enum Event: EventType {
|
||||
/// The flow is being started.
|
||||
case start
|
||||
|
||||
/// Request presentation of the settings screen.
|
||||
case showSettingsScreen
|
||||
/// The settings screen has been dismissed.
|
||||
case dismissedSettingsScreen
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
private let actionsSubject: PassthroughSubject<UserSessionFlowCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<UserSessionFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
isNewLogin: Bool,
|
||||
navigationRootCoordinator: NavigationRootCoordinator,
|
||||
appLockService: AppLockServiceProtocol,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
@@ -51,17 +79,21 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
appHooks: AppHooks,
|
||||
analytics: AnalyticsService,
|
||||
notificationManager: NotificationManagerProtocol,
|
||||
isNewLogin: Bool) {
|
||||
stateMachineFactory: StateMachineFactoryProtocol) {
|
||||
self.userSession = userSession
|
||||
self.navigationRootCoordinator = navigationRootCoordinator
|
||||
self.appLockService = appLockService
|
||||
self.bugReportService = bugReportService
|
||||
self.appMediator = appMediator
|
||||
self.appSettings = appSettings
|
||||
self.analytics = analytics
|
||||
|
||||
navigationTabCoordinator = NavigationTabCoordinator()
|
||||
navigationRootCoordinator.setRootCoordinator(navigationTabCoordinator)
|
||||
|
||||
let chatsSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
|
||||
chatsFlowCoordinator = ChatsFlowCoordinator(userSession: userSession,
|
||||
isNewLogin: isNewLogin,
|
||||
navigationSplitCoordinator: chatsSplitCoordinator,
|
||||
appLockService: appLockService,
|
||||
bugReportService: bugReportService,
|
||||
@@ -72,7 +104,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
appHooks: appHooks,
|
||||
analytics: analytics,
|
||||
notificationManager: notificationManager,
|
||||
isNewLogin: isNewLogin)
|
||||
stateMachineFactory: stateMachineFactory)
|
||||
chatsTabDetails = .init(tag: HomeTab.chats, title: L10n.screenHomeTabChats, icon: \.chat, selectedIcon: \.chatSolid)
|
||||
chatsTabDetails.barVisibility = .hidden
|
||||
|
||||
@@ -98,16 +130,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.init(coordinator: spacesSplitCoordinator, details: spacesTabDetails)
|
||||
])
|
||||
|
||||
stateMachine = stateMachineFactory.makeUserSessionFlowStateMachine(state: .initial)
|
||||
configureStateMachine()
|
||||
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
func start() {
|
||||
#warning("This flow still needs a state machine.")
|
||||
|
||||
chatsFlowCoordinator.start()
|
||||
spaceExplorerFlowCoordinator.start()
|
||||
|
||||
attemptStartingOnboarding()
|
||||
stateMachine.tryEvent(.start)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
@@ -115,13 +145,31 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
// There aren't any routes that directly target this flow yet, so pass them directly to the
|
||||
// chats flow coordinator.
|
||||
chatsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
navigationTabCoordinator.selectedTab = .chats
|
||||
switch appRoute {
|
||||
case .accountProvisioningLink:
|
||||
break // We always ignore this flow when logged in.
|
||||
case .settings, .chatBackupSettings:
|
||||
if stateMachine.state != .settingsScreen {
|
||||
stateMachine.tryEvent(.showSettingsScreen)
|
||||
}
|
||||
settingsFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
||||
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
|
||||
.roomDetails, .roomMemberDetails, .userProfile,
|
||||
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
|
||||
.call, .genericCallLink, .share, .transferOwnership:
|
||||
clearRoute(animated: animated) // Make sure the presented route is visible.
|
||||
chatsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
navigationTabCoordinator.selectedTab = .chats
|
||||
}
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
switch stateMachine.state {
|
||||
case .initial, .tabBar:
|
||||
break
|
||||
case .settingsScreen:
|
||||
navigationTabCoordinator.setSheetCoordinator(nil, animated: animated)
|
||||
}
|
||||
chatsFlowCoordinator.clearRoute(animated: animated)
|
||||
}
|
||||
|
||||
@@ -132,19 +180,40 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func configureStateMachine() {
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .tabBar]) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
chatsFlowCoordinator.start()
|
||||
spaceExplorerFlowCoordinator.start()
|
||||
attemptStartingOnboarding()
|
||||
}
|
||||
|
||||
stateMachine.addRoutes(event: .showSettingsScreen, transitions: [.tabBar => .settingsScreen]) { [weak self] _ in
|
||||
self?.startSettingsFlow()
|
||||
}
|
||||
stateMachine.addRoutes(event: .dismissedSettingsScreen, transitions: [.settingsScreen => .tabBar]) { [weak self] _ in
|
||||
self?.settingsFlowCoordinator = nil
|
||||
}
|
||||
|
||||
stateMachine.addErrorHandler { context in
|
||||
fatalError("Unexpected transition: \(context)")
|
||||
}
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
chatsFlowCoordinator.actionsPublisher
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .logout:
|
||||
Task { await self.runLogoutFlow() }
|
||||
case .showSettings:
|
||||
handleAppRoute(.settings, animated: true)
|
||||
case .showChatBackupSettings:
|
||||
handleAppRoute(.chatBackupSettings, animated: true)
|
||||
case .sessionVerification(let flow):
|
||||
presentSessionVerificationScreen(flow: flow)
|
||||
case .clearCache:
|
||||
actionsSubject.send(.clearCache)
|
||||
case .forceLogout:
|
||||
actionsSubject.send(.forceLogout)
|
||||
case .logout:
|
||||
Task { await self.runLogoutFlow() }
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -154,7 +223,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .showSettings:
|
||||
break
|
||||
stateMachine.tryEvent(.showSettingsScreen)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -198,6 +267,51 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
private func startSettingsFlow() {
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = SettingsFlowCoordinator(parameters: .init(userSession: userSession,
|
||||
windowManager: appMediator.windowManager,
|
||||
appLockService: appLockService,
|
||||
bugReportService: bugReportService,
|
||||
notificationSettings: userSession.clientProxy.notificationSettings,
|
||||
secureBackupController: userSession.clientProxy.secureBackupController,
|
||||
appSettings: appSettings,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: analytics))
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
navigationTabCoordinator.setSheetCoordinator(nil)
|
||||
case .clearCache:
|
||||
actionsSubject.send(.clearCache)
|
||||
case .runLogoutFlow:
|
||||
Task {
|
||||
self.navigationTabCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
// The sheet needs to be dismissed before the alert can be shown
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
await self.runLogoutFlow()
|
||||
}
|
||||
case .forceLogout:
|
||||
actionsSubject.send(.forceLogout)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
settingsFlowCoordinator = coordinator
|
||||
coordinator.handleAppRoute(.settings, animated: false)
|
||||
|
||||
navigationTabCoordinator.setSheetCoordinator(navigationStackCoordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissedSettingsScreen)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Session Verification
|
||||
|
||||
private func setupSessionVerificationRequestsObserver() {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
protocol StateMachineFactoryProtocol {
|
||||
func makeUserSessionFlowStateMachine(state: UserSessionFlowCoordinator.State) -> StateMachine<UserSessionFlowCoordinator.State, UserSessionFlowCoordinator.Event>
|
||||
func makeChatsFlowStateMachine() -> ChatsFlowCoordinatorStateMachine
|
||||
}
|
||||
|
||||
struct StateMachineFactory: StateMachineFactoryProtocol {
|
||||
func makeUserSessionFlowStateMachine(state: UserSessionFlowCoordinator.State) -> StateMachine<UserSessionFlowCoordinator.State, UserSessionFlowCoordinator.Event> {
|
||||
.init(state: state)
|
||||
}
|
||||
|
||||
func makeChatsFlowStateMachine() -> ChatsFlowCoordinatorStateMachine {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: For testing
|
||||
|
||||
class PublishedStateMachineFactory: StateMachineFactoryProtocol {
|
||||
let baseFactory = StateMachineFactory()
|
||||
|
||||
// MARK: UserSessionFlowCoordinator
|
||||
|
||||
let userSessionFlowStatePublisher = PassthroughSubject<UserSessionFlowCoordinator.State, Never>()
|
||||
|
||||
func makeUserSessionFlowStateMachine(state: UserSessionFlowCoordinator.State) -> StateMachine<UserSessionFlowCoordinator.State, UserSessionFlowCoordinator.Event> {
|
||||
let stateMachine = baseFactory.makeUserSessionFlowStateMachine(state: state)
|
||||
stateMachine.addAnyHandler(.any => .any) { [weak self] in self?.userSessionFlowStatePublisher.send($0.toState) }
|
||||
return stateMachine
|
||||
}
|
||||
|
||||
// MARK: ChatsFlowCoordinator
|
||||
|
||||
let chatsFlowStatePublisher = PassthroughSubject<ChatsFlowCoordinatorStateMachine.State, Never>()
|
||||
|
||||
func makeChatsFlowStateMachine() -> ChatsFlowCoordinatorStateMachine {
|
||||
let stateMachine = baseFactory.makeChatsFlowStateMachine()
|
||||
stateMachine.addTransitionHandler { [weak self] in self?.chatsFlowStatePublisher.send($0.toState) }
|
||||
return stateMachine
|
||||
}
|
||||
}
|
||||
@@ -589,6 +589,7 @@ class MockScreen: Identifiable {
|
||||
appMediator.underlyingWindowManager = windowManager
|
||||
|
||||
let flowCoordinator = UserSessionFlowCoordinator(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
isNewLogin: false,
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
appLockService: AppLockService(keychainController: KeychainControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings),
|
||||
@@ -600,7 +601,7 @@ class MockScreen: Identifiable {
|
||||
appHooks: AppHooks(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
notificationManager: NotificationManagerMock(),
|
||||
isNewLogin: false)
|
||||
stateMachineFactory: StateMachineFactory())
|
||||
|
||||
flowCoordinator.start()
|
||||
|
||||
@@ -746,6 +747,7 @@ class MockScreen: Identifiable {
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
let flowCoordinator = ChatsFlowCoordinator(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
isNewLogin: false,
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
appLockService: AppLockService(keychainController: KeychainControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings),
|
||||
@@ -757,7 +759,7 @@ class MockScreen: Identifiable {
|
||||
appHooks: AppHooks(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
notificationManager: NotificationManagerMock(),
|
||||
isNewLogin: false)
|
||||
stateMachineFactory: StateMachineFactory())
|
||||
|
||||
flowCoordinator.start()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class ChatsFlowCoordinatorTests: XCTestCase {
|
||||
var chatsFlowCoordinator: ChatsFlowCoordinator!
|
||||
var splitCoordinator: NavigationSplitCoordinator!
|
||||
var notificationManager: NotificationManagerMock!
|
||||
let stateMachineFactory = PublishedStateMachineFactory()
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@@ -33,6 +34,7 @@ class ChatsFlowCoordinatorTests: XCTestCase {
|
||||
notificationManager = NotificationManagerMock()
|
||||
|
||||
chatsFlowCoordinator = ChatsFlowCoordinator(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
isNewLogin: false,
|
||||
navigationSplitCoordinator: splitCoordinator,
|
||||
appLockService: AppLockServiceMock(),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
@@ -43,9 +45,9 @@ class ChatsFlowCoordinatorTests: XCTestCase {
|
||||
appHooks: AppHooks(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
notificationManager: notificationManager,
|
||||
isNewLogin: false)
|
||||
stateMachineFactory: stateMachineFactory)
|
||||
|
||||
let deferred = deferFulfillment(chatsFlowCoordinator.statePublisher) { $0 == .roomList(roomListSelectedRoomID: nil) }
|
||||
let deferred = deferFulfillment(stateMachineFactory.chatsFlowStatePublisher) { $0 == .roomList(roomListSelectedRoomID: nil) }
|
||||
chatsFlowCoordinator.start()
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
@@ -127,7 +129,7 @@ class ChatsFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
|
||||
let unexpectedFulfillment = deferFailure(chatsFlowCoordinator.statePublisher, timeout: 1) { _ in true }
|
||||
let unexpectedFulfillment = deferFailure(stateMachineFactory.chatsFlowStatePublisher, timeout: 1) { _ in true }
|
||||
chatsFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
|
||||
try await unexpectedFulfillment.fulfill()
|
||||
|
||||
@@ -150,7 +152,7 @@ class ChatsFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
|
||||
let unexpectedFulfillment = deferFailure(chatsFlowCoordinator.statePublisher, timeout: 1) { _ in true }
|
||||
let unexpectedFulfillment = deferFailure(stateMachineFactory.chatsFlowStatePublisher, timeout: 1) { _ in true }
|
||||
chatsFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
|
||||
try await unexpectedFulfillment.fulfill()
|
||||
|
||||
@@ -241,17 +243,6 @@ class ChatsFlowCoordinatorTests: XCTestCase {
|
||||
"A new timeline should be created for the same room ID, so that the screen isn't stale while loading.")
|
||||
}
|
||||
|
||||
func testShareMediaRouteWithoutRoom() async throws {
|
||||
try await process(route: .settings, expectedState: .settingsScreen(roomListSelectedRoomID: nil))
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: nil, mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedState: .shareExtensionRoomList(sharePayload: sharePayload))
|
||||
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareMediaRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1"))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
@@ -264,17 +255,6 @@ class ChatsFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithoutRoom() async throws {
|
||||
try await process(route: .settings, expectedState: .settingsScreen(roomListSelectedRoomID: nil))
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: nil, text: "Important Text")
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedState: .shareExtensionRoomList(sharePayload: sharePayload))
|
||||
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1"))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
@@ -291,7 +271,7 @@ class ChatsFlowCoordinatorTests: XCTestCase {
|
||||
|
||||
private func process(route: AppRoute, expectedState: ChatsFlowCoordinatorStateMachine.State) async throws {
|
||||
// Sometimes the state machine's state changes before the coordinators have updated the stack.
|
||||
let delayedPublisher = chatsFlowCoordinator.statePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)
|
||||
let delayedPublisher = stateMachineFactory.chatsFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)
|
||||
|
||||
let deferred = deferFulfillment(delayedPublisher) { $0 == expectedState }
|
||||
chatsFlowCoordinator.handleAppRoute(route, animated: true)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// Copyright 2023, 2024 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 XCTest
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
var clientProxy: ClientProxyMock!
|
||||
var timelineControllerFactory: TimelineControllerFactoryMock!
|
||||
var userSessionFlowCoordinator: UserSessionFlowCoordinator!
|
||||
var rootCoordinator: NavigationRootCoordinator!
|
||||
var notificationManager: NotificationManagerMock!
|
||||
let stateMachineFactory = PublishedStateMachineFactory()
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var tabCoordinator: NavigationTabCoordinator<UserSessionFlowCoordinator.HomeTab>? { rootCoordinator?.rootCoordinator as? NavigationTabCoordinator }
|
||||
var chatsSplitCoordinator: NavigationSplitCoordinator? { tabCoordinator?.tabCoordinators.first as? NavigationSplitCoordinator }
|
||||
var detailCoordinator: CoordinatorProtocol? { chatsSplitCoordinator?.detailCoordinator }
|
||||
var detailNavigationStack: NavigationStackCoordinator? { detailCoordinator as? NavigationStackCoordinator }
|
||||
|
||||
override func setUp() async throws {
|
||||
cancellables.removeAll()
|
||||
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
||||
timelineControllerFactory = TimelineControllerFactoryMock(.init())
|
||||
|
||||
rootCoordinator = NavigationRootCoordinator()
|
||||
|
||||
notificationManager = NotificationManagerMock()
|
||||
|
||||
userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
isNewLogin: false,
|
||||
navigationRootCoordinator: rootCoordinator,
|
||||
appLockService: AppLockServiceMock(),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
notificationManager: notificationManager,
|
||||
stateMachineFactory: stateMachineFactory)
|
||||
|
||||
userSessionFlowCoordinator.start()
|
||||
}
|
||||
|
||||
func testInitialState() async throws {
|
||||
XCTAssertNotNil(chatsSplitCoordinator)
|
||||
XCTAssertNil(detailCoordinator)
|
||||
}
|
||||
|
||||
func testSettingsPresentation() async throws {
|
||||
try await process(route: .settings, expectedUserSessionState: .settingsScreen)
|
||||
XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
}
|
||||
|
||||
func testRoomPresentation() async throws {
|
||||
try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(roomListSelectedRoomID: "1"))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
}
|
||||
|
||||
func testShareMediaRouteWithoutRoom() async throws {
|
||||
try await process(route: .settings, expectedUserSessionState: .settingsScreen)
|
||||
XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: nil, mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedUserSessionState: .tabBar,
|
||||
expectedChatsState: .shareExtensionRoomList(sharePayload: sharePayload))
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareMediaRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(roomListSelectedRoomID: "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"))
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithoutRoom() async throws {
|
||||
try await process(route: .settings, expectedUserSessionState: .settingsScreen)
|
||||
XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: nil, text: "Important Text")
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedUserSessionState: .tabBar,
|
||||
expectedChatsState: .shareExtensionRoomList(sharePayload: sharePayload))
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(roomListSelectedRoomID: "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"))
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func process(route: AppRoute,
|
||||
expectedUserSessionState: UserSessionFlowCoordinator.State? = nil,
|
||||
expectedChatsState: ChatsFlowCoordinatorStateMachine.State? = nil) async throws {
|
||||
let deferredUserSession: DeferredFulfillment? = if let expectedUserSessionState {
|
||||
deferFulfillment(stateMachineFactory.userSessionFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)) {
|
||||
$0 == expectedUserSessionState
|
||||
}
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
let deferredChatsState: DeferredFulfillment? = if let expectedChatsState {
|
||||
deferFulfillment(stateMachineFactory.chatsFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)) {
|
||||
$0 == expectedChatsState
|
||||
}
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
||||
try await deferredUserSession?.fulfill()
|
||||
try await deferredChatsState?.fulfill()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user