Beam/ElementX/Sources/AppCoordinator.swift

452 lines
19 KiB
Swift
Raw Normal View History

2022-02-14 18:05:21 +02:00
//
// Copyright 2022 New Vector Ltd
2022-02-14 18:05:21 +02:00
//
// 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.
2022-02-14 18:05:21 +02:00
//
import Combine
import MatrixRustSDK
import UIKit
2022-02-14 18:05:21 +02:00
struct ServiceLocator {
fileprivate static var serviceLocator: ServiceLocator?
static var shared: ServiceLocator {
guard let serviceLocator = serviceLocator else {
fatalError("The service locator should be setup at this point")
}
return serviceLocator
}
let userIndicatorPresenter: UserIndicatorTypePresenter
}
2022-02-14 18:05:21 +02:00
class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private let window: UIWindow
private let stateMachine: AppCoordinatorStateMachine
2022-02-14 18:05:21 +02:00
private let mainNavigationController: UINavigationController
private let splashViewController: UIViewController
private let navigationRouter: NavigationRouter
private let userSessionStore: UserSessionStoreProtocol
2022-02-14 18:05:21 +02:00
private var userSession: UserSessionProtocol!
private let memberDetailProviderManager: MemberDetailProviderManager
2022-06-06 12:38:07 +03:00
private let bugReportService: BugReportServiceProtocol
private let screenshotDetector: ScreenshotDetector
private let backgroundTaskService: BackgroundTaskServiceProtocol
2022-06-06 12:38:07 +03:00
private var loadingIndicator: UserIndicator?
private var statusIndicator: UserIndicator?
2022-02-14 18:05:21 +02:00
var childCoordinators: [Coordinator] = []
init() {
stateMachine = AppCoordinatorStateMachine()
bugReportService = BugReportService(withBaseURL: BuildSettings.bugReportServiceBaseURL, sentryURL: BuildSettings.bugReportSentryURL)
2022-06-06 12:38:07 +03:00
2022-02-14 18:05:21 +02:00
splashViewController = SplashViewController()
mainNavigationController = ElementNavigationController(rootViewController: splashViewController)
2022-02-14 18:05:21 +02:00
window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = mainNavigationController
window.tintColor = .element.accent
2022-02-14 18:05:21 +02:00
navigationRouter = NavigationRouter(navigationController: mainNavigationController)
memberDetailProviderManager = MemberDetailProviderManager()
ServiceLocator.serviceLocator = ServiceLocator(userIndicatorPresenter: UserIndicatorTypePresenter(presentingViewController: mainNavigationController))
2022-02-14 18:05:21 +02:00
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
fatalError("Should have a valid bundle identifier at this point")
}
backgroundTaskService = UIKitBackgroundTaskService(withApplication: UIApplication.shared)
2022-02-14 18:05:21 +02:00
userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier,
backgroundTaskService: backgroundTaskService)
2022-06-06 12:38:07 +03:00
screenshotDetector = ScreenshotDetector()
screenshotDetector.callback = processScreenshotDetection
setupStateMachine()
setupLogging()
// Benchmark.trackingEnabled = true
2022-02-14 18:05:21 +02:00
}
func start() {
2022-06-14 18:04:42 +01:00
window.makeKeyAndVisible()
stateMachine.processEvent(userSessionStore.hasSessions ? .startWithExistingSession : .startWithAuthentication)
2022-02-14 18:05:21 +02:00
}
// MARK: - AuthenticationCoordinatorDelegate
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
self.userSession = userSession
remove(childCoordinator: authenticationCoordinator)
stateMachine.processEvent(.succeededSigningIn)
}
// MARK: - Private
private func setupLogging() {
let loggerConfiguration = MXLogConfiguration()
#if DEBUG
// This exposes the full Rust side tracing subscriber filter for more flexibility.
// We can filter by level, crate and even file. See more details here:
// https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples
setupTracing(configuration: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn")
loggerConfiguration.logLevel = .debug
#else
setupTracing(configuration: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn")
loggerConfiguration.logLevel = .info
#endif
// Avoid redirecting NSLogs to files if we are attached to a debugger.
if isatty(STDERR_FILENO) == 0 {
loggerConfiguration.redirectLogsToFiles = true
}
MXLog.configure(loggerConfiguration)
}
// swiftlint:disable cyclomatic_complexity
private func setupStateMachine() {
stateMachine.addTransitionHandler { [weak self] context in
guard let self = self else { return }
switch (context.fromState, context.event, context.toState) {
case (.initial, .startWithAuthentication, .signedOut):
self.startAuthentication()
case (.signedOut, .succeededSigningIn, .homeScreen):
self.presentHomeScreen()
case (.initial, .startWithExistingSession, .restoringSession):
self.showLoadingIndicator()
self.restoreUserSession()
case (.restoringSession, .failedRestoringSession, .signedOut):
self.hideLoadingIndicator()
self.showLoginErrorToast()
case (.restoringSession, .succeededRestoringSession, .homeScreen):
self.hideLoadingIndicator()
self.presentHomeScreen()
case(_, _, .roomScreen(let roomId)):
self.presentRoomWithIdentifier(roomId)
case(.roomScreen, .dismissedRoomScreen, .homeScreen):
self.tearDownDismissedRoomScreen()
case (_, .attemptSignOut, .signingOut):
self.userSessionStore.logout(userSession: self.userSession)
self.stateMachine.processEvent(.succeededSigningOut)
case (.signingOut, .succeededSigningOut, .signedOut):
self.tearDownUserSession()
case (.signingOut, .failedSigningOut, _):
self.showLogoutErrorToast()
2022-06-06 12:38:07 +03:00
case (.homeScreen, .showSettingsScreen, .settingsScreen):
self.presentSettingsScreen()
case (.settingsScreen, .dismissedSettingsScreen, .homeScreen):
self.tearDownDismissedSettingsScreen()
case (.homeScreen, .showSessionVerificationScreen, .sessionVerificationScreen):
self.presentSessionVerification()
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .homeScreen):
self.tearDownDismissedSessionVerificationScreen()
default:
fatalError("Unknown transition: \(context)")
}
}
stateMachine.addErrorHandler { context in
fatalError("Failed transition with context: \(context)")
}
}
// swiftlint:enable cyclomatic_complexity function_body_length
private func restoreUserSession() {
Task {
switch await userSessionStore.restoreUserSession() {
case .success(let userSession):
self.userSession = userSession
stateMachine.processEvent(.succeededRestoringSession)
case .failure:
MXLog.error("Failed to restore an existing session.")
stateMachine.processEvent(.failedRestoringSession)
}
}
}
private func startAuthentication() {
let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore)
let coordinator = AuthenticationCoordinator(authenticationService: authenticationService,
navigationRouter: navigationRouter)
coordinator.delegate = self
add(childCoordinator: coordinator)
coordinator.start()
2022-02-14 18:05:21 +02:00
}
private func tearDownUserSession() {
if let presentedCoordinator = childCoordinators.first {
remove(childCoordinator: presentedCoordinator)
}
userSession = nil
mainNavigationController.setViewControllers([splashViewController], animated: false)
startAuthentication()
2022-02-14 18:05:21 +02:00
}
private func presentHomeScreen() {
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(),
memberDetailProviderManager: memberDetailProviderManager)
2022-03-17 18:09:29 +02:00
let coordinator = HomeScreenCoordinator(parameters: parameters)
2022-02-14 18:05:21 +02:00
coordinator.callback = { [weak self] action in
guard let self = self else { return }
switch action {
2022-06-06 12:38:07 +03:00
case .presentRoom(let roomIdentifier):
self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
2022-06-06 12:38:07 +03:00
case .presentSettings:
self.stateMachine.processEvent(.showSettingsScreen)
case .presentBugReport:
self.presentBugReportScreen()
case .verifySession:
self.stateMachine.processEvent(.showSessionVerificationScreen)
case .signOut:
self.confirmSignOut()
}
}
2022-02-14 18:05:21 +02:00
add(childCoordinator: coordinator)
navigationRouter.setRootModule(coordinator)
2022-06-06 12:38:07 +03:00
if bugReportService.crashedLastRun {
showCrashPopup()
}
}
2022-02-14 18:05:21 +02:00
// MARK: Rooms
private func presentRoomWithIdentifier(_ roomIdentifier: String) {
guard let roomProxy = userSession.clientProxy.rooms.first(where: { $0.id == roomIdentifier }) else {
MXLog.error("Invalid room identifier: \(roomIdentifier)")
return
}
let userId = userSession.clientProxy.userIdentifier
let memberDetailProvider = memberDetailProviderManager.memberDetailProviderForRoomProxy(roomProxy)
let timelineItemFactory = RoomTimelineItemFactory(mediaProvider: userSession.mediaProvider,
memberDetailProvider: memberDetailProvider,
attributedStringBuilder: AttributedStringBuilder())
let timelineController = RoomTimelineController(userId: userId,
roomId: roomIdentifier,
timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider,
memberDetailProvider: memberDetailProvider)
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
roomName: roomProxy.displayName ?? roomProxy.name,
roomAvatar: userSession.mediaProvider.imageFromURLString(roomProxy.avatarURL),
roomEncryptionBadge: roomProxy.encryptionBadgeImage)
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() {
guard let coordinator = childCoordinators.last as? RoomScreenCoordinator else {
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
}
remove(childCoordinator: coordinator)
}
// MARK: Settings
private func presentSettingsScreen() {
let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter,
2022-09-02 10:49:59 +01:00
userSession: userSession,
bugReportService: bugReportService)
let coordinator = SettingsCoordinator(parameters: parameters)
coordinator.callback = { [weak self] action in
guard let self = self else { return }
switch action {
case .logout:
self.stateMachine.processEvent(.attemptSignOut)
}
}
2022-06-06 12:38:07 +03:00
add(childCoordinator: coordinator)
coordinator.start()
navigationRouter.push(coordinator) { [weak self] in
guard let self = self else { return }
self.stateMachine.processEvent(.dismissedSettingsScreen)
}
}
2022-06-06 12:38:07 +03:00
private func tearDownDismissedSettingsScreen() {
guard let coordinator = childCoordinators.last as? SettingsCoordinator else {
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
}
remove(childCoordinator: coordinator)
}
2022-06-06 12:38:07 +03:00
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 confirmSignOut() {
let alert = UIAlertController(title: ElementL10n.actionSignOut,
message: ElementL10n.actionSignOutConfirmationSimple,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel))
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { [weak self] _ in
self?.stateMachine.processEvent(.attemptSignOut)
})
navigationRouter.present(alert, animated: true)
}
2022-06-06 12:38:07 +03:00
private func processScreenshotDetection(image: UIImage?, error: Error?) {
MXLog.debug("Detected screenshot: \(String(describing: image)), error: \(String(describing: error))")
2022-06-06 12:38:07 +03:00
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())
2022-06-06 12:38:07 +03:00
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")
2022-06-06 12:38:07 +03:00
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() {
loadingIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
}
private func hideLoadingIndicator() {
loadingIndicator = nil
}
private func showLoginErrorToast() {
statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging in"))
}
private func showLogoutErrorToast() {
statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging out"))
}
2022-02-14 18:05:21 +02:00
}