379 lines
14 KiB
Swift
Executable File
379 lines
14 KiB
Swift
Executable File
/*
|
|
Copyright 2020-2024 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import Combine
|
|
import Foundation
|
|
import Intents
|
|
import MatrixSDK
|
|
import CommonKit
|
|
import UIKit
|
|
|
|
#if DEBUG
|
|
import FLEX
|
|
#endif
|
|
|
|
/// The AppCoordinator is responsible of screen navigation and data injection at root application level. It decides
|
|
/// if authentication or home screen should be shown and inject data needed for these flows, it changes the navigation
|
|
/// stack on deep link, displays global warning.
|
|
/// This class should avoid to contain too many data management code not related to screen navigation logic. For example
|
|
/// `MXSession` or push notification management should be handled in dedicated classes and report only navigation
|
|
/// changes to the AppCoordinator.
|
|
final class AppCoordinator: NSObject, AppCoordinatorType {
|
|
|
|
// MARK: - Constants
|
|
|
|
// MARK: - Properties
|
|
|
|
private let customSchemeURLParser: CustomSchemeURLParser
|
|
|
|
// MARK: Private
|
|
|
|
private let rootRouter: RootRouterType
|
|
// swiftlint:disable weak_delegate
|
|
fileprivate let legacyAppDelegate: LegacyAppDelegate = AppDelegate.theDelegate()
|
|
// swiftlint:enable weak_delegate
|
|
|
|
private lazy var appNavigator: AppNavigatorProtocol = {
|
|
return AppNavigator(appCoordinator: self)
|
|
}()
|
|
|
|
fileprivate weak var splitViewCoordinator: SplitViewCoordinatorType?
|
|
fileprivate weak var sideMenuCoordinator: SideMenuCoordinatorType?
|
|
|
|
private let userSessionsService: UserSessionsService
|
|
|
|
/// Main user Matrix session
|
|
private var mainMatrixSession: MXSession? {
|
|
return self.userSessionsService.mainUserSession?.matrixSession
|
|
}
|
|
|
|
private var currentSpaceId: String?
|
|
private var cancellables: Set<AnyCancellable> = .init()
|
|
private var pushRulesUpdater: PushRulesUpdater?
|
|
|
|
// MARK: Public
|
|
|
|
var childCoordinators: [Coordinator] = []
|
|
|
|
// MARK: - Setup
|
|
|
|
init(router: RootRouterType, window: UIWindow) {
|
|
self.rootRouter = router
|
|
self.customSchemeURLParser = CustomSchemeURLParser()
|
|
self.userSessionsService = UserSessionsService.shared
|
|
|
|
super.init()
|
|
|
|
setupFlexDebuggerOnWindow(window)
|
|
update(with: ThemeService.shared().theme)
|
|
}
|
|
|
|
// MARK: - Public methods
|
|
|
|
func start() {
|
|
setupLogger()
|
|
setupTheme()
|
|
excludeAllItemsFromBackup()
|
|
setupPushRulesSessionEvents()
|
|
|
|
// Setup navigation router store
|
|
_ = NavigationRouterStore.shared
|
|
|
|
// Setup user location services
|
|
_ = UserLocationServiceProvider.shared
|
|
|
|
if BuildSettings.enableSideMenu {
|
|
self.addSideMenu()
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(forName: NSNotification.Name.appDelegateNetworkStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] notification in
|
|
guard let self = self else { return }
|
|
|
|
if AppDelegate.theDelegate().isOffline {
|
|
self.splitViewCoordinator?.showAppStateIndicator(with: VectorL10n.networkOfflineTitle, icon: UIImage(systemName: "wifi.slash"))
|
|
} else {
|
|
self.splitViewCoordinator?.hideAppStateIndicator()
|
|
}
|
|
}
|
|
|
|
// NOTE: When split view is shown there can be no Matrix sessions ready. Keep this behavior or use a loading screen before showing the split view.
|
|
self.showSplitView()
|
|
MXLog.debug("[AppCoordinator] Showed split view")
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(self.themeDidChange), name: Notification.Name.themeServiceDidChangeTheme, object: nil)
|
|
}
|
|
|
|
func open(url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
|
// NOTE: As said in the Apple documentation be careful on security issues with Custom Scheme URL:
|
|
// https://developer.apple.com/documentation/xcode/allowing_apps_and_websites_to_link_to_your_content/defining_a_custom_url_scheme_for_your_app
|
|
|
|
do {
|
|
let deepLinkOption = try self.customSchemeURLParser.parse(url: url, options: options)
|
|
return self.handleDeepLinkOption(deepLinkOption)
|
|
} catch {
|
|
MXLog.debug("[AppCoordinator] Custom scheme URL parsing failed with error: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Theme management
|
|
|
|
@objc private func themeDidChange() {
|
|
update(with: ThemeService.shared().theme)
|
|
}
|
|
|
|
private func update(with theme: Theme) {
|
|
for window in UIApplication.shared.windows {
|
|
window.overrideUserInterfaceStyle = ThemeService.shared().theme.userInterfaceStyle
|
|
}
|
|
}
|
|
|
|
// MARK: - Private methods
|
|
private func setupLogger() {
|
|
UILog.configure(logger: MatrixSDKLogger.self)
|
|
}
|
|
|
|
private func setupTheme() {
|
|
ThemeService.shared().themeId = RiotSettings.shared.userInterfaceTheme
|
|
|
|
// Set theme id from current theme.identifier, themeId can be nil.
|
|
if let themeId = ThemeIdentifier(rawValue: ThemeService.shared().theme.identifier) {
|
|
ThemePublisher.configure(themeId: themeId)
|
|
} else {
|
|
MXLog.error("[AppCoordinator] No theme id found to update ThemePublisher")
|
|
}
|
|
|
|
// Always republish theme change events, and again always getting the identifier from the theme.
|
|
let themeIdPublisher = NotificationCenter.default.publisher(for: Notification.Name.themeServiceDidChangeTheme)
|
|
.compactMap({ _ in ThemeIdentifier(rawValue: ThemeService.shared().theme.identifier) })
|
|
.eraseToAnyPublisher()
|
|
|
|
ThemePublisher.shared.republish(themeIdPublisher: themeIdPublisher)
|
|
}
|
|
|
|
private func excludeAllItemsFromBackup() {
|
|
let manager = FileManager.default
|
|
|
|
// Individual files and directories created by the application or SDK are excluded case-by-case,
|
|
// but sometimes the lifecycle of a file is not directly controlled by the app (e.g. plists for
|
|
// UserDefaults). For that reason the app will always exclude all top-level directories as well
|
|
// as individual files.
|
|
manager.excludeAllUserDirectoriesFromBackup()
|
|
manager.excludeAllAppGroupDirectoriesFromBackup()
|
|
}
|
|
|
|
private func showAuthentication() {
|
|
// TODO: Implement
|
|
}
|
|
|
|
private func showLoading() {
|
|
// TODO: Implement
|
|
}
|
|
|
|
private func showPinCode() {
|
|
// TODO: Implement
|
|
}
|
|
|
|
private func showSplitView() {
|
|
let coordinatorParameters = SplitViewCoordinatorParameters(router: self.rootRouter, userSessionsService: self.userSessionsService, appNavigator: self.appNavigator)
|
|
|
|
let splitViewCoordinator = SplitViewCoordinator(parameters: coordinatorParameters)
|
|
splitViewCoordinator.delegate = self
|
|
splitViewCoordinator.start()
|
|
self.add(childCoordinator: splitViewCoordinator)
|
|
self.splitViewCoordinator = splitViewCoordinator
|
|
}
|
|
|
|
private func addSideMenu() {
|
|
let appInfo = AppInfo.current
|
|
let coordinatorParameters = SideMenuCoordinatorParameters(appNavigator: self.appNavigator, userSessionsService: self.userSessionsService, appInfo: appInfo)
|
|
|
|
let coordinator = SideMenuCoordinator(parameters: coordinatorParameters)
|
|
coordinator.delegate = self
|
|
coordinator.start()
|
|
self.add(childCoordinator: coordinator)
|
|
self.sideMenuCoordinator = coordinator
|
|
}
|
|
|
|
private func checkAppVersion() {
|
|
// TODO: Implement
|
|
}
|
|
|
|
private func handleDeepLinkOption(_ deepLinkOption: DeepLinkOption) -> Bool {
|
|
|
|
let canOpenLink: Bool
|
|
|
|
switch deepLinkOption {
|
|
case .connect(let loginToken, let transactionID):
|
|
canOpenLink = AuthenticationService.shared.continueSSOLogin(with: loginToken, and: transactionID)
|
|
}
|
|
|
|
return canOpenLink
|
|
}
|
|
|
|
private func setupFlexDebuggerOnWindow(_ window: UIWindow) {
|
|
#if DEBUG
|
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showFlexDebugger))
|
|
tapGestureRecognizer.numberOfTouchesRequired = 2
|
|
tapGestureRecognizer.numberOfTapsRequired = 2
|
|
window.addGestureRecognizer(tapGestureRecognizer)
|
|
#endif
|
|
}
|
|
|
|
@objc private func showFlexDebugger() {
|
|
#if DEBUG
|
|
FLEXManager.shared.showExplorer()
|
|
#endif
|
|
}
|
|
|
|
fileprivate func navigate(to destination: AppNavigatorDestination) {
|
|
switch destination {
|
|
case .homeSpace:
|
|
MXLog.verbose("Switch to home space")
|
|
self.navigateToSpace(with: nil)
|
|
Analytics.shared.activeSpace = nil
|
|
case .space(let spaceId):
|
|
MXLog.verbose("Switch to space with id: \(spaceId)")
|
|
self.navigateToSpace(with: spaceId)
|
|
Analytics.shared.activeSpace = userSessionsService.mainUserSession?.matrixSession.spaceService.getSpace(withId: spaceId)
|
|
}
|
|
}
|
|
|
|
private func navigateToSpace(with spaceId: String?) {
|
|
guard spaceId != self.currentSpaceId else {
|
|
MXLog.verbose("Space with id: \(String(describing: spaceId)) is already selected")
|
|
return
|
|
}
|
|
|
|
self.currentSpaceId = spaceId
|
|
|
|
// Reload split view with selected space id
|
|
self.splitViewCoordinator?.start(with: spaceId)
|
|
}
|
|
|
|
private func setupPushRulesSessionEvents() {
|
|
let sessionReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange)
|
|
.compactMap { $0.object as? MXSession }
|
|
.filter { $0.state == .running }
|
|
.removeDuplicates { session1, session2 in
|
|
session1 == session2
|
|
}
|
|
|
|
sessionReady
|
|
.sink { [weak self] session in
|
|
self?.setupPushRulesUpdater(session: session)
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
|
|
let sessionClosed = NotificationCenter.default.publisher(for: .mxSessionStateDidChange)
|
|
.compactMap { $0.object as? MXSession }
|
|
.filter { $0.state == .closed }
|
|
|
|
sessionClosed
|
|
.sink { [weak self] _ in
|
|
self?.pushRulesUpdater = nil
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
private func setupPushRulesUpdater(session: MXSession) {
|
|
pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: session))
|
|
|
|
let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput()
|
|
let needsCheckPublisher = applicationDidBecomeActive.merge(with: Just(())).eraseToAnyPublisher()
|
|
|
|
needsCheckPublisher
|
|
.sink { _ in
|
|
Task { @MainActor [weak self] in
|
|
await self?.pushRulesUpdater?.syncRulesIfNeeded()
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
}
|
|
|
|
// MARK: - LegacyAppDelegateDelegate
|
|
extension AppCoordinator: LegacyAppDelegateDelegate {
|
|
|
|
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, wantsToPopToHomeViewControllerAnimated animated: Bool, completion: (() -> Void)!) {
|
|
|
|
MXLog.debug("[AppCoordinator] wantsToPopToHomeViewControllerAnimated")
|
|
|
|
self.splitViewCoordinator?.popToHome(animated: animated, completion: completion)
|
|
}
|
|
|
|
func legacyAppDelegateRestoreEmptyDetailsViewController(_ legacyAppDelegate: LegacyAppDelegate!) {
|
|
self.splitViewCoordinator?.resetDetails(animated: false)
|
|
}
|
|
|
|
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didAddMatrixSession session: MXSession!) {
|
|
}
|
|
|
|
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didRemoveMatrixSession session: MXSession?) {
|
|
guard let session = session else { return }
|
|
// Handle user session removal on clear cache. On clear cache the account has his session closed but the account is not removed.
|
|
self.userSessionsService.removeUserSession(relatedToMatrixSession: session)
|
|
}
|
|
|
|
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didAdd account: MXKAccount!) {
|
|
self.userSessionsService.addUserSession(fromAccount: account)
|
|
}
|
|
|
|
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didRemove account: MXKAccount!) {
|
|
self.userSessionsService.removeUserSession(relatedToAccount: account)
|
|
}
|
|
|
|
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didNavigateToSpaceWithId spaceId: String!) {
|
|
self.sideMenuCoordinator?.select(spaceWithId: spaceId)
|
|
}
|
|
}
|
|
|
|
// MARK: - SplitViewCoordinatorDelegate
|
|
extension AppCoordinator: SplitViewCoordinatorDelegate {
|
|
func splitViewCoordinatorDidCompleteAuthentication(_ coordinator: SplitViewCoordinatorType) {
|
|
self.legacyAppDelegate.authenticationDidComplete()
|
|
}
|
|
}
|
|
|
|
// MARK: - SideMenuCoordinatorDelegate
|
|
extension AppCoordinator: SideMenuCoordinatorDelegate {
|
|
func sideMenuCoordinator(_ coordinator: SideMenuCoordinatorType, didTapMenuItem menuItem: SideMenuItem, fromSourceView sourceView: UIView) {
|
|
}
|
|
}
|
|
|
|
// MARK: - AppNavigator
|
|
|
|
// swiftlint:disable private_over_fileprivate
|
|
fileprivate class AppNavigator: AppNavigatorProtocol {
|
|
// swiftlint:enable private_over_fileprivate
|
|
|
|
// MARK: - Properties
|
|
|
|
private unowned let appCoordinator: AppCoordinator
|
|
|
|
lazy var sideMenu: SideMenuPresentable = {
|
|
guard let sideMenuCoordinator = appCoordinator.sideMenuCoordinator else {
|
|
fatalError("sideMenuCoordinator is not initialized")
|
|
}
|
|
|
|
return SideMenuPresenter(sideMenuCoordinator: sideMenuCoordinator)
|
|
}()
|
|
|
|
// MARK: - Setup
|
|
|
|
init(appCoordinator: AppCoordinator) {
|
|
self.appCoordinator = appCoordinator
|
|
}
|
|
|
|
// MARK: - Public
|
|
|
|
func navigate(to destination: AppNavigatorDestination) {
|
|
self.appCoordinator.navigate(to: destination)
|
|
}
|
|
}
|