mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:06:00 +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:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user