Beam/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift
Stefan Ceriu b122b02bee
Share extension (#3506)
* Setup simple share extension

* Switch the app url scheme to be the full bundle identifier

* Setup a share extension that show a SwiftUI view, uses rust tracing and redirects to the hosting aplication

* Move media as json through the custom scheme into the main app and deep link into the media upload preview screen

* Fix message forwarding and global search screen room summary provider filtering.

* Tweak the message forwarding and global search screen designs.

* Add a room selection screen to use after receiving a share request from the share extension

* Fix share extension entitlements

* Share the temporary directory between the main app and the extensions; rename the caches one.

* Remove the no longer needed notification avatar flipping fix.

* Extract the placeholder avatar image generator from the NSE

* Nest `AvatarSize` within the new `Avatars` enum

* Donate an `INSendMessageIntent` to the system every time we send a message so they appear as share suggestions

* Support suggestions in the share extension itself

* Improve sharing animations and fix presentation when room already on the stack

* Clear all routes when sharing without a preselected room.

* Fix broken unit tests

* Various initial tweaks following code review.

* Correctly clean up and dismiss the share extension for all paths.

* Move the share extension path to a constants enum

* Rename UserSessionFlowCoordinator specific share extension states and events

* Add UserSession and Room flow coordinator share route tests

* Tweak the share extension logic.
2024-11-13 14:02:47 +02:00

199 lines
7.9 KiB
Swift

//
// Copyright 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 SwiftState
enum EncryptionSettingsFlowCoordinatorAction: Equatable {
/// The flow is complete.
case complete
}
struct EncryptionSettingsFlowCoordinatorParameters {
let userSession: UserSessionProtocol
let appSettings: AppSettings
let userIndicatorController: UserIndicatorControllerProtocol
let navigationStackCoordinator: NavigationStackCoordinator
}
class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
private let userSession: UserSessionProtocol
private let appSettings: AppSettings
private let userIndicatorController: UserIndicatorControllerProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
// periphery:ignore - retaining purpose
private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator?
enum State: StateType {
/// The state machine hasn't started.
case initial
/// The root screen for this flow.
case secureBackupScreen
/// The user is managing their recovery key.
case recoveryKeyScreen
/// The user is disabling key backups.
case keyBackupScreen
}
enum Event: EventType {
/// The flow is being started.
case start
/// The user would like to manage their recovery key.
case manageRecoveryKey
/// The user finished managing their recovery key.
case finishedManagingRecoveryKey
/// The user doesn't want to use key backup any more.
case disableKeyBackup
/// The key backup screen was dismissed.
case finishedDisablingKeyBackup
}
private let stateMachine: StateMachine<State, Event>
private var cancellables: Set<AnyCancellable> = []
private let actionsSubject: PassthroughSubject<EncryptionSettingsFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<EncryptionSettingsFlowCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: EncryptionSettingsFlowCoordinatorParameters) {
userSession = parameters.userSession
appSettings = parameters.appSettings
userIndicatorController = parameters.userIndicatorController
navigationStackCoordinator = parameters.navigationStackCoordinator
stateMachine = .init(state: .initial)
configureStateMachine()
}
func start() {
stateMachine.tryEvent(.start)
}
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
switch appRoute {
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
.roomDetails, .roomMemberDetails, .userProfile,
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
.call, .genericCallLink, .settings, .share:
// These routes aren't in this flow so clear the entire stack.
clearRoute(animated: animated)
case .chatBackupSettings:
popToRootScreen(animated: animated)
}
}
func clearRoute(animated: Bool) {
let fromState = stateMachine.state
popToRootScreen(animated: animated)
guard fromState != .initial else { return }
navigationStackCoordinator.pop(animated: animated) // SecureBackup screen.
}
func popToRootScreen(animated: Bool) {
// As we push screens on top of an existing stack, a literal pop to root wouldn't be safe.
switch stateMachine.state {
case .initial, .secureBackupScreen:
break
case .recoveryKeyScreen:
navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // RecoveryKey screen.
case .keyBackupScreen:
navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // KeyBackup screen.
}
}
// MARK: - Private
private func configureStateMachine() {
stateMachine.addRoutes(event: .start, transitions: [.initial => .secureBackupScreen]) { [weak self] _ in
self?.presentSecureBackupScreen()
}
stateMachine.addRoutes(event: .manageRecoveryKey, transitions: [.secureBackupScreen => .recoveryKeyScreen]) { [weak self] _ in
self?.presentRecoveryKeyScreen()
}
stateMachine.addRoutes(event: .finishedManagingRecoveryKey, transitions: [.recoveryKeyScreen => .secureBackupScreen])
stateMachine.addRoutes(event: .disableKeyBackup, transitions: [.secureBackupScreen => .keyBackupScreen]) { [weak self] _ in
self?.presentKeyBackupScreen()
}
stateMachine.addRoutes(event: .finishedDisablingKeyBackup, transitions: [.keyBackupScreen => .secureBackupScreen])
stateMachine.addErrorHandler { context in
fatalError("Unexpected transition: \(context)")
}
}
private func presentSecureBackupScreen(animated: Bool = true) {
let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: appSettings,
clientProxy: userSession.clientProxy,
userIndicatorController: userIndicatorController))
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .manageRecoveryKey:
stateMachine.tryEvent(.manageRecoveryKey)
case .disableKeyBackup:
stateMachine.tryEvent(.disableKeyBackup)
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
self?.actionsSubject.send(.complete)
}
}
private func presentRecoveryKeyScreen() {
let sheetNavigationStackCoordinator = NavigationStackCoordinator()
let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
userIndicatorController: userIndicatorController,
isModallyPresented: true))
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
sheetNavigationStackCoordinator.setRootCoordinator(coordinator, animated: true)
navigationStackCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator) { [stateMachine] in
stateMachine.tryEvent(.finishedManagingRecoveryKey)
}
}
private func presentKeyBackupScreen() {
let sheetNavigationStackCoordinator = NavigationStackCoordinator()
let coordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
userIndicatorController: userIndicatorController))
coordinator.actions.sink { [weak self] action in
switch action {
case .done:
self?.navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
sheetNavigationStackCoordinator.setRootCoordinator(coordinator, animated: true)
navigationStackCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator) { [stateMachine] in
stateMachine.tryEvent(.finishedDisablingKeyBackup)
}
}
}