mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Introduced a UserSessionFlowCoordinator and split up the AppCoordinator state machine (#212)
This commit is contained in:
+25
-228
@@ -52,8 +52,9 @@ class AppCoordinator: Coordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
|
||||
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let screenshotDetector: ScreenshotDetector
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
private var loadingIndicator: UserIndicator?
|
||||
@@ -89,9 +90,6 @@ class AppCoordinator: Coordinator {
|
||||
|
||||
userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
|
||||
screenshotDetector = ScreenshotDetector()
|
||||
screenshotDetector.callback = processScreenshotDetection
|
||||
|
||||
setupStateMachine()
|
||||
|
||||
@@ -108,7 +106,7 @@ class AppCoordinator: Coordinator {
|
||||
func stop() {
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupLogging() {
|
||||
@@ -142,43 +140,29 @@ class AppCoordinator: Coordinator {
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .startWithAuthentication, .signedOut):
|
||||
self.startAuthentication()
|
||||
case (.signedOut, .succeededSigningIn, .homeScreen):
|
||||
self.presentHomeScreen()
|
||||
|
||||
case (.signedOut, .succeededSigningIn, .signedIn):
|
||||
self.setupUserSession()
|
||||
case (.initial, .startWithExistingSession, .restoringSession):
|
||||
self.showLoadingIndicator()
|
||||
self.restoreUserSession()
|
||||
case (.restoringSession, .failedRestoringSession, .signedOut):
|
||||
self.hideLoadingIndicator()
|
||||
self.showLoginErrorToast()
|
||||
case (.restoringSession, .succeededRestoringSession, .homeScreen):
|
||||
case (.restoringSession, .succeededRestoringSession, .signedIn):
|
||||
self.hideLoadingIndicator()
|
||||
self.presentHomeScreen()
|
||||
|
||||
case(_, _, .roomScreen(let roomId)):
|
||||
self.presentRoomWithIdentifier(roomId)
|
||||
case(.roomScreen(let roomId), .dismissedRoomScreen, .homeScreen):
|
||||
self.tearDownDismissedRoomScreen(roomId)
|
||||
|
||||
self.setupUserSession()
|
||||
case (_, .signOut, .signingOut):
|
||||
self.showLoadingIndicator()
|
||||
self.tearDownUserSession()
|
||||
case (.signingOut, .completedSigningOut, .signedOut):
|
||||
self.presentSplashScreen()
|
||||
self.hideLoadingIndicator()
|
||||
|
||||
case (_, .remoteSignOut(let isSoft), .remoteSigningOut):
|
||||
self.showLoadingIndicator()
|
||||
self.tearDownUserSession(isSoftLogout: isSoft)
|
||||
case (.remoteSigningOut(let isSoft), .completedSigningOut, .signedOut):
|
||||
self.presentSplashScreen(isSoftLogout: isSoft)
|
||||
self.hideLoadingIndicator()
|
||||
|
||||
case (.homeScreen, .showSessionVerificationScreen, .sessionVerificationScreen):
|
||||
self.presentSessionVerification()
|
||||
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .homeScreen):
|
||||
self.tearDownDismissedSessionVerificationScreen()
|
||||
|
||||
default:
|
||||
fatalError("Unknown transition: \(context)")
|
||||
}
|
||||
@@ -257,8 +241,26 @@ class AppCoordinator: Coordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUserSession() {
|
||||
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
|
||||
navigationRouter: navigationRouter,
|
||||
bugReportService: bugReportService)
|
||||
|
||||
userSessionFlowCoordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
case .signOut:
|
||||
self?.stateMachine.processEvent(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
userSessionFlowCoordinator.start()
|
||||
|
||||
self.userSessionFlowCoordinator = userSessionFlowCoordinator
|
||||
}
|
||||
|
||||
private func tearDownUserSession(isSoftLogout: Bool = false) {
|
||||
userSession.clientProxy.stopSync()
|
||||
userSessionFlowCoordinator?.stop()
|
||||
|
||||
deobserveUserSessionChanges()
|
||||
|
||||
@@ -291,38 +293,6 @@ class AppCoordinator: Coordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentHomeScreen() {
|
||||
userSession.clientProxy.startSync()
|
||||
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentRoom(let roomIdentifier):
|
||||
self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
|
||||
case .presentSettings:
|
||||
self.presentSettingsScreen()
|
||||
case .presentBugReport:
|
||||
self.presentBugReportScreen()
|
||||
case .verifySession:
|
||||
self.stateMachine.processEvent(.showSessionVerificationScreen)
|
||||
case .signOut:
|
||||
self.stateMachine.processEvent(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
navigationRouter.setRootModule(coordinator)
|
||||
|
||||
if bugReportService.crashedLastRun {
|
||||
showCrashPopup()
|
||||
}
|
||||
}
|
||||
|
||||
private func observeUserSessionChanges() {
|
||||
userSession.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -347,179 +317,6 @@ class AppCoordinator: Coordinator {
|
||||
cancellables.removeAll()
|
||||
}
|
||||
|
||||
// MARK: Rooms
|
||||
|
||||
private func presentRoomWithIdentifier(_ roomIdentifier: String) {
|
||||
guard let roomProxy = userSession.clientProxy.roomForIdentifier(roomIdentifier) else {
|
||||
MXLog.error("Invalid room identifier: \(roomIdentifier)")
|
||||
return
|
||||
}
|
||||
let userId = userSession.clientProxy.userIdentifier
|
||||
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userId,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomProxy: roomProxy,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
|
||||
let timelineController = RoomTimelineController(userId: userId,
|
||||
roomId: roomIdentifier,
|
||||
timelineProvider: roomProxy.timelineProvider,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomProxy: roomProxy)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomName: roomProxy.displayName ?? roomProxy.name,
|
||||
roomAvatarUrl: roomProxy.avatarURL)
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.stateMachine.processEvent(.dismissedRoomScreen)
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownDismissedRoomScreen(_ roomId: String) {
|
||||
guard let coordinator = childCoordinators.last as? RoomScreenCoordinator else {
|
||||
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
|
||||
}
|
||||
|
||||
remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
|
||||
private func presentSettingsScreen() {
|
||||
let navController = ElementNavigationController()
|
||||
let newNavigationRouter = NavigationRouter(navigationController: navController)
|
||||
|
||||
let parameters = SettingsCoordinatorParameters(navigationRouter: newNavigationRouter,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
let coordinator = SettingsCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self.dismissSettingsScreen()
|
||||
case .logout:
|
||||
self.dismissSettingsScreen()
|
||||
self.stateMachine.processEvent(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navController.viewControllers = [coordinator.toPresentable()]
|
||||
navigationRouter.present(navController, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func dismissSettingsScreen() {
|
||||
MXLog.debug("dismissSettingsScreen")
|
||||
|
||||
guard let coordinator = childCoordinators.first(where: { $0 is SettingsCoordinator }) else {
|
||||
return
|
||||
}
|
||||
|
||||
navigationRouter.dismissModule()
|
||||
remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
private func showCrashPopup() {
|
||||
let alert = UIAlertController(title: nil,
|
||||
message: ElementL10n.sendBugReportAppCrashed,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.no, style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.yes, style: .default) { [weak self] _ in
|
||||
self?.presentBugReportScreen()
|
||||
})
|
||||
|
||||
navigationRouter.present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func processScreenshotDetection(image: UIImage?, error: Error?) {
|
||||
MXLog.debug("Detected screenshot: \(String(describing: image)), error: \(String(describing: error))")
|
||||
|
||||
let alert = UIAlertController(title: ElementL10n.screenshotDetectedTitle,
|
||||
message: ElementL10n.screenshotDetectedMessage,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.no, style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.yes, style: .default) { [weak self] _ in
|
||||
self?.presentBugReportScreen(for: image)
|
||||
})
|
||||
|
||||
navigationRouter.present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentBugReportScreen(for image: UIImage? = nil) {
|
||||
let parameters = BugReportCoordinatorParameters(bugReportService: bugReportService,
|
||||
screenshot: image)
|
||||
let coordinator = BugReportCoordinator(parameters: parameters)
|
||||
coordinator.completion = { [weak self, weak coordinator] in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
self.navigationRouter.dismissModule(animated: true)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
let navController = ElementNavigationController(rootViewController: coordinator.toPresentable())
|
||||
navController.navigationBar.topItem?.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(dismissBugReportScreen))
|
||||
navController.isModalInPresentation = true
|
||||
navigationRouter.present(navController, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func dismissBugReportScreen() {
|
||||
MXLog.debug("dismissBugReportScreen")
|
||||
|
||||
guard let bugReportCoordinator = childCoordinators.first(where: { $0 is BugReportCoordinator }) else {
|
||||
return
|
||||
}
|
||||
|
||||
navigationRouter.dismissModule()
|
||||
remove(childCoordinator: bugReportCoordinator)
|
||||
}
|
||||
|
||||
// MARK: Session verification
|
||||
|
||||
private func presentSessionVerification() {
|
||||
Task {
|
||||
guard let sessionVerificationController = userSession.sessionVerificationController else {
|
||||
fatalError("The sessionVerificationController should aways be valid at this point")
|
||||
}
|
||||
|
||||
let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
|
||||
|
||||
let coordinator = SessionVerificationCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] in
|
||||
self?.navigationRouter.dismissModule()
|
||||
self?.stateMachine.processEvent(.dismissedSessionVerificationScreen)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
navigationRouter.present(coordinator)
|
||||
|
||||
coordinator.start()
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownDismissedSessionVerificationScreen() {
|
||||
guard let coordinator = childCoordinators.last as? SessionVerificationCoordinator else {
|
||||
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
|
||||
}
|
||||
|
||||
remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
// MARK: Toasts and loading indicators
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
+5
-29
@@ -26,15 +26,9 @@ class AppCoordinatorStateMachine {
|
||||
case signedOut
|
||||
/// Opening an existing session.
|
||||
case restoringSession
|
||||
/// Showing the home screen
|
||||
case homeScreen
|
||||
|
||||
/// Showing a particular room's timeline
|
||||
/// - Parameter roomId: that room's identifier
|
||||
case roomScreen(roomId: String)
|
||||
|
||||
/// Showing the session verification flows
|
||||
case sessionVerificationScreen
|
||||
/// User session started
|
||||
case signedIn
|
||||
|
||||
/// Processing a sign out request
|
||||
case signingOut
|
||||
@@ -63,17 +57,6 @@ class AppCoordinatorStateMachine {
|
||||
case remoteSignOut(isSoft: Bool)
|
||||
/// Signing out completed
|
||||
case completedSigningOut
|
||||
|
||||
/// Request presentation for a particular room
|
||||
/// - Parameter roomId:the room identifier
|
||||
case showRoomScreen(roomId: String)
|
||||
/// The room screen has been dismissed
|
||||
case dismissedRoomScreen
|
||||
|
||||
/// Request the start of the session verification flow
|
||||
case showSessionVerificationScreen
|
||||
/// Session verification has finished
|
||||
case dismissedSessionVerificationScreen
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
@@ -81,25 +64,18 @@ class AppCoordinatorStateMachine {
|
||||
init() {
|
||||
stateMachine = StateMachine(state: .initial) { machine in
|
||||
machine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut])
|
||||
machine.addRoutes(event: .succeededSigningIn, transitions: [.signedOut => .homeScreen])
|
||||
machine.addRoutes(event: .succeededSigningIn, transitions: [.signedOut => .signedIn])
|
||||
|
||||
machine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
|
||||
machine.addRoutes(event: .succeededRestoringSession, transitions: [.restoringSession => .homeScreen])
|
||||
machine.addRoutes(event: .succeededRestoringSession, transitions: [.restoringSession => .signedIn])
|
||||
machine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
|
||||
|
||||
machine.addRoutes(event: .signOut, transitions: [.any => .signingOut])
|
||||
machine.addRoutes(event: .completedSigningOut, transitions: [.signingOut => .signedOut])
|
||||
|
||||
machine.addRoutes(event: .showSessionVerificationScreen, transitions: [.homeScreen => .sessionVerificationScreen])
|
||||
machine.addRoutes(event: .dismissedSessionVerificationScreen, transitions: [.sessionVerificationScreen => .homeScreen])
|
||||
|
||||
|
||||
// Transitions with associated values need to be handled through `addRouteMapping`
|
||||
machine.addRouteMapping { event, fromState, _ in
|
||||
switch (event, fromState) {
|
||||
case (.showRoomScreen(let roomId), .homeScreen):
|
||||
return .roomScreen(roomId: roomId)
|
||||
case (.dismissedRoomScreen, .roomScreen):
|
||||
return .homeScreen
|
||||
case (.remoteSignOut(let isSoft), _):
|
||||
return .remoteSigningOut(isSoft: isSoft)
|
||||
case (.completedSigningOut, .remoteSigningOut):
|
||||
+2
-2
@@ -40,14 +40,14 @@ final class BuildSettings {
|
||||
static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: ElementInfoPlist.cfBundleIdentifier.starts(with: "io.element.elementx"),
|
||||
host: "https://posthog.element.dev",
|
||||
apiKey: "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN",
|
||||
termsURL: URL(string: "https://element.io/cookie-policy")!) // swiftlint:disable:this force_unwrapping
|
||||
termsURL: URL(staticString: "https://element.io/cookie-policy"))
|
||||
#else
|
||||
/// The configuration to use for analytics. Set `isEnabled` to false to disable analytics.
|
||||
/// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations.
|
||||
static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: ElementInfoPlist.cfBundleIdentifier.starts(with: "io.element.elementx"),
|
||||
host: "https://posthog.hss.element.io",
|
||||
apiKey: "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
|
||||
termsURL: URL(string: "https://element.io/cookie-policy")!) // swiftlint:disable:this force_unwrapping
|
||||
termsURL: URL(staticString: "https://element.io/cookie-policy"))
|
||||
#endif
|
||||
|
||||
// MARK: - Settings screen
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
extension ElementL10n {
|
||||
/// Main menu
|
||||
/// User menu
|
||||
public static let a11yAllChatsUserAvatarMenu = ElementL10n.tr("Untranslated", "a11y_all_chats_user_avatar_menu")
|
||||
/// Confirm
|
||||
public static let actionConfirm = ElementL10n.tr("Untranslated", "action_confirm")
|
||||
|
||||
@@ -128,4 +128,16 @@ extension NavigationRouterType {
|
||||
push(modules.map { $0.toModule() },
|
||||
animated: animated)
|
||||
}
|
||||
|
||||
func dismissModule(animated: Bool = true, completion: (() -> Void)? = nil) {
|
||||
dismissModule(animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func push(_ module: Presentable, animated: Bool = true, popCompletion: (() -> Void)? = nil) {
|
||||
push(module, animated: animated, popCompletion: popCompletion)
|
||||
}
|
||||
|
||||
func present(_ module: Presentable, animated: Bool = true) {
|
||||
present(module, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
enum UserSessionFlowCoordinatorAction {
|
||||
case signOut
|
||||
}
|
||||
|
||||
class UserSessionFlowCoordinator: Coordinator {
|
||||
private let stateMachine: UserSessionFlowCoordinatorStateMachine
|
||||
|
||||
private let userSession: UserSessionProtocol
|
||||
private let navigationRouter: NavigationRouterType
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
var callback: ((UserSessionFlowCoordinatorAction) -> Void)?
|
||||
|
||||
init(userSession: UserSessionProtocol, navigationRouter: NavigationRouterType, bugReportService: BugReportServiceProtocol) {
|
||||
stateMachine = UserSessionFlowCoordinatorStateMachine()
|
||||
self.userSession = userSession
|
||||
self.navigationRouter = navigationRouter
|
||||
self.bugReportService = bugReportService
|
||||
|
||||
setupStateMachine()
|
||||
}
|
||||
|
||||
func start() {
|
||||
stateMachine.processEvent(.start)
|
||||
}
|
||||
|
||||
func stop() { }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupStateMachine() {
|
||||
stateMachine.addTransitionHandler { [weak self] context in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .start, .homeScreen):
|
||||
self.presentHomeScreen()
|
||||
|
||||
case(_, _, .roomScreen(let roomId)):
|
||||
self.presentRoomWithIdentifier(roomId)
|
||||
case(.roomScreen(let roomId), .dismissedRoomScreen, .homeScreen):
|
||||
self.tearDownDismissedRoomScreen(roomId)
|
||||
|
||||
case (.homeScreen, .showSessionVerificationScreen, .sessionVerificationScreen):
|
||||
self.presentSessionVerification()
|
||||
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .homeScreen):
|
||||
self.tearDownDismissedSessionVerificationScreen()
|
||||
|
||||
case (.homeScreen, .showSettingsScreen, .settingsScreen):
|
||||
self.presentSettingsScreen()
|
||||
case (.settingsScreen, .dismissedSettingsScreen, .homeScreen):
|
||||
self.dismissSettingsScreen()
|
||||
|
||||
default:
|
||||
fatalError("Unknown transition: \(context)")
|
||||
}
|
||||
}
|
||||
|
||||
stateMachine.addErrorHandler { context in
|
||||
fatalError("Failed transition with context: \(context)")
|
||||
}
|
||||
}
|
||||
|
||||
private func presentHomeScreen() {
|
||||
userSession.clientProxy.startSync()
|
||||
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentRoom(let roomIdentifier):
|
||||
self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
|
||||
case .presentSettings:
|
||||
self.presentSettingsScreen()
|
||||
case .presentBugReport:
|
||||
self.presentBugReportScreen()
|
||||
case .verifySession:
|
||||
self.stateMachine.processEvent(.showSessionVerificationScreen)
|
||||
case .signOut:
|
||||
self.callback?(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
navigationRouter.setRootModule(coordinator)
|
||||
|
||||
if bugReportService.crashedLastRun {
|
||||
showCrashPopup()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Rooms
|
||||
|
||||
private func presentRoomWithIdentifier(_ roomIdentifier: String) {
|
||||
guard let roomProxy = userSession.clientProxy.roomForIdentifier(roomIdentifier) else {
|
||||
MXLog.error("Invalid room identifier: \(roomIdentifier)")
|
||||
return
|
||||
}
|
||||
let userId = userSession.clientProxy.userIdentifier
|
||||
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userId,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomProxy: roomProxy,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
|
||||
let timelineController = RoomTimelineController(userId: userId,
|
||||
roomId: roomIdentifier,
|
||||
timelineProvider: roomProxy.timelineProvider,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomProxy: roomProxy)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomName: roomProxy.displayName ?? roomProxy.name,
|
||||
roomAvatarUrl: roomProxy.avatarURL)
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.stateMachine.processEvent(.dismissedRoomScreen)
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownDismissedRoomScreen(_ roomId: String) {
|
||||
guard let coordinator = childCoordinators.last as? RoomScreenCoordinator else {
|
||||
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
|
||||
}
|
||||
|
||||
remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
|
||||
private func presentSettingsScreen() {
|
||||
let navController = ElementNavigationController()
|
||||
let newNavigationRouter = NavigationRouter(navigationController: navController)
|
||||
|
||||
let parameters = SettingsCoordinatorParameters(navigationRouter: newNavigationRouter,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
let coordinator = SettingsCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self.dismissSettingsScreen()
|
||||
case .logout:
|
||||
self.dismissSettingsScreen()
|
||||
self.callback?(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navController.viewControllers = [coordinator.toPresentable()]
|
||||
navigationRouter.present(navController, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func dismissSettingsScreen() {
|
||||
MXLog.debug("dismissSettingsScreen")
|
||||
|
||||
guard let coordinator = childCoordinators.first(where: { $0 is SettingsCoordinator }) else {
|
||||
return
|
||||
}
|
||||
|
||||
navigationRouter.dismissModule()
|
||||
remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
// MARK: Session verification
|
||||
|
||||
private func presentSessionVerification() {
|
||||
Task {
|
||||
guard let sessionVerificationController = userSession.sessionVerificationController else {
|
||||
fatalError("The sessionVerificationController should aways be valid at this point")
|
||||
}
|
||||
|
||||
let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
|
||||
|
||||
let coordinator = SessionVerificationCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] in
|
||||
self?.navigationRouter.dismissModule()
|
||||
self?.stateMachine.processEvent(.dismissedSessionVerificationScreen)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
navigationRouter.present(coordinator)
|
||||
|
||||
coordinator.start()
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownDismissedSessionVerificationScreen() {
|
||||
guard let coordinator = childCoordinators.last as? SessionVerificationCoordinator else {
|
||||
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
|
||||
}
|
||||
|
||||
remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
// MARK: Bug reporting
|
||||
|
||||
private func showCrashPopup() {
|
||||
let alert = UIAlertController(title: nil,
|
||||
message: ElementL10n.sendBugReportAppCrashed,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.no, style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.yes, style: .default) { [weak self] _ in
|
||||
self?.presentBugReportScreen()
|
||||
})
|
||||
|
||||
navigationRouter.present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentBugReportScreen(for image: UIImage? = nil) {
|
||||
let parameters = BugReportCoordinatorParameters(bugReportService: bugReportService,
|
||||
screenshot: image)
|
||||
let coordinator = BugReportCoordinator(parameters: parameters)
|
||||
coordinator.completion = { [weak self, weak coordinator] in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
self.navigationRouter.dismissModule(animated: true)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
let navController = ElementNavigationController(rootViewController: coordinator.toPresentable())
|
||||
navController.navigationBar.topItem?.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(dismissBugReportScreen))
|
||||
navController.isModalInPresentation = true
|
||||
navigationRouter.present(navController, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func dismissBugReportScreen() {
|
||||
MXLog.debug("dismissBugReportScreen")
|
||||
|
||||
guard let bugReportCoordinator = childCoordinators.first(where: { $0 is BugReportCoordinator }) else {
|
||||
return
|
||||
}
|
||||
|
||||
navigationRouter.dismissModule()
|
||||
remove(childCoordinator: bugReportCoordinator)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftState
|
||||
|
||||
class UserSessionFlowCoordinatorStateMachine {
|
||||
/// States the AppCoordinator can find itself in
|
||||
enum State: StateType {
|
||||
/// The initial state, used before the coordinator starts
|
||||
case initial
|
||||
|
||||
/// Showing the home screen
|
||||
case homeScreen
|
||||
|
||||
/// Showing a particular room's timeline
|
||||
/// - Parameter roomId: that room's identifier
|
||||
case roomScreen(roomId: String)
|
||||
|
||||
/// Showing the session verification flows
|
||||
case sessionVerificationScreen
|
||||
|
||||
/// Showing the settings screen
|
||||
case settingsScreen
|
||||
}
|
||||
|
||||
/// Events that can be triggered on the AppCoordinator state machine
|
||||
enum Event: EventType {
|
||||
/// Start the user session flows
|
||||
case start
|
||||
|
||||
/// Request presentation for a particular room
|
||||
/// - Parameter roomId:the room identifier
|
||||
case showRoomScreen(roomId: String)
|
||||
/// The room screen has been dismissed
|
||||
case dismissedRoomScreen
|
||||
|
||||
/// Request the start of the session verification flow
|
||||
case showSessionVerificationScreen
|
||||
/// Session verification has finished
|
||||
case dismissedSessionVerificationScreen
|
||||
|
||||
/// Request presentation of the settings screen
|
||||
case showSettingsScreen
|
||||
/// The settings screen has been dismissed
|
||||
case dismissedSettingsScreen
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
|
||||
init() {
|
||||
stateMachine = StateMachine(state: .initial) { machine in
|
||||
machine.addRoutes(event: .start, transitions: [.initial => .homeScreen])
|
||||
|
||||
// Transitions with associated values need to be handled through `addRouteMapping`
|
||||
machine.addRouteMapping { event, fromState, _ in
|
||||
switch (event, fromState) {
|
||||
case (.showRoomScreen(let roomId), .homeScreen):
|
||||
return .roomScreen(roomId: roomId)
|
||||
case (.dismissedRoomScreen, .roomScreen):
|
||||
return .homeScreen
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to move the state machine to another state through an event
|
||||
/// It will either invoke the `transitionHandler` or the `errorHandler` depending on its current state
|
||||
func processEvent(_ event: Event) {
|
||||
stateMachine.tryEvent(event)
|
||||
}
|
||||
|
||||
/// Registers a callback for processing state machine transitions
|
||||
func addTransitionHandler(_ handler: @escaping StateMachine<State, Event>.Handler) {
|
||||
stateMachine.addAnyHandler(.any => .any, handler: handler)
|
||||
}
|
||||
|
||||
/// Registers a callback for processing state machine errors
|
||||
func addErrorHandler(_ handler: @escaping StateMachine<State, Event>.Handler) {
|
||||
stateMachine.addErrorHandler(handler: handler)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user