Beam/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift
manuroe 3950cac085
Dual licensing: AGPL + Element Commercial (#3657)
* New LICENSE-COMMERCIAL file

* Apply dual licenses: AGPL + Element Commercial to file headers

* Update README with dual licensing
2025-01-06 11:27:37 +01:00

203 lines
8.9 KiB
Swift

//
// Copyright 2024 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
enum RoomRolesAndPermissionsFlowCoordinatorAction: Equatable {
/// The flow is complete.
case complete
}
struct RoomRolesAndPermissionsFlowCoordinatorParameters {
let roomProxy: JoinedRoomProxyProtocol
let mediaProvider: MediaProviderProtocol
let navigationStackCoordinator: NavigationStackCoordinator
let userIndicatorController: UserIndicatorControllerProtocol
let analytics: AnalyticsService
}
class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol {
private let roomProxy: JoinedRoomProxyProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
private let mediaProvider: MediaProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let analytics: AnalyticsService
enum State: StateType {
/// The state machine hasn't started.
case initial
/// The root screen for this flow.
case rolesAndPermissionsScreen
/// Changing member roles.
case changingRoles
/// Changing room permissions.
case changingPermissions
/// The flow is complete and the stack has been cleaned up.
case complete
}
enum Event: EventType {
/// The flow is being started.
case start
/// The user would like to change member roles.
case changeRoles
/// The user finished changing member roles.
case finishedChangingRoles
/// The user would like to change room permissions.
case changePermissions
/// The user finished changing room permissions.
case finishedChangingPermissions
/// The user has demoted themself.
case demotedOwnUser
}
private let stateMachine: StateMachine<State, Event>
private var cancellables: Set<AnyCancellable> = []
private let actionsSubject: PassthroughSubject<RoomRolesAndPermissionsFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<RoomRolesAndPermissionsFlowCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: RoomRolesAndPermissionsFlowCoordinatorParameters) {
roomProxy = parameters.roomProxy
navigationStackCoordinator = parameters.navigationStackCoordinator
mediaProvider = parameters.mediaProvider
userIndicatorController = parameters.userIndicatorController
analytics = parameters.analytics
stateMachine = .init(state: .initial)
configureStateMachine()
}
func start() {
stateMachine.tryEvent(.start)
}
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
// There aren't any routes to this screen, so always clear the stack.
clearRoute(animated: animated)
}
func clearRoute(animated: Bool) {
// As we push screens on top of an existing stack, popping to root wouldn't be safe.
switch stateMachine.state {
case .initial, .complete:
break
case .rolesAndPermissionsScreen:
navigationStackCoordinator.pop(animated: animated)
case .changingRoles, .changingPermissions:
navigationStackCoordinator.pop(animated: animated) // ChangeRoles or ChangePermissions screen.
navigationStackCoordinator.pop(animated: animated) // RolesAndPermissions screen.
}
}
// MARK: - Private
private func configureStateMachine() {
stateMachine.addRoutes(event: .start, transitions: [.initial => .rolesAndPermissionsScreen]) { [weak self] _ in
self?.presentRolesAndPermissionsScreen()
}
stateMachine.addRoutes(event: .changeRoles, transitions: [.rolesAndPermissionsScreen => .changingRoles]) { [weak self] context in
guard let role = context.userInfo as? RoomRolesAndPermissionsScreenRole else { fatalError("Expected a role") }
self?.presentChangeRolesScreen(role: role)
}
stateMachine.addRoutes(event: .finishedChangingRoles, transitions: [.changingRoles => .rolesAndPermissionsScreen])
stateMachine.addRoutes(event: .changePermissions, transitions: [.rolesAndPermissionsScreen => .changingPermissions]) { [weak self] context in
guard let (permissions, group) = context.userInfo as? (RoomPermissions, RoomRolesAndPermissionsScreenPermissionsGroup) else {
fatalError("Expected a group and the current permissions")
}
self?.presentChangePermissionsScreen(permissions: permissions, group: group)
}
stateMachine.addRoutes(event: .finishedChangingPermissions, transitions: [.changingPermissions => .rolesAndPermissionsScreen])
stateMachine.addRoutes(event: .demotedOwnUser, transitions: [.rolesAndPermissionsScreen => .complete]) { [weak self] _ in
self?.navigationStackCoordinator.pop()
}
stateMachine.addErrorHandler { context in
fatalError("Unexpected transition: \(context)")
}
}
private func presentRolesAndPermissionsScreen() {
let parameters = RoomRolesAndPermissionsScreenCoordinatorParameters(roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
analytics: analytics)
let coordinator = RoomRolesAndPermissionsScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher.sink { [stateMachine] action in
switch action {
case .editRoles(let role):
stateMachine.tryEvent(.changeRoles, userInfo: role)
case .editPermissions(let permissions, let group):
stateMachine.tryEvent(.changePermissions, userInfo: (permissions, group))
case .demotedOwnUser:
stateMachine.tryEvent(.demotedOwnUser)
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.actionsSubject.send(.complete)
}
}
private func presentChangeRolesScreen(role: RoomRolesAndPermissionsScreenRole) {
let mode = switch role {
case .administrators: RoomMemberDetails.Role.administrator
case .moderators: RoomMemberDetails.Role.moderator
}
let parameters = RoomChangeRolesScreenCoordinatorParameters(mode: mode,
roomProxy: roomProxy,
mediaProvider: mediaProvider,
userIndicatorController: userIndicatorController,
analytics: analytics)
let coordinator = RoomChangeRolesScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
// When discarding changes is finalised, either use an event or remove this action.
navigationStackCoordinator.pop()
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator) { [stateMachine] in
stateMachine.tryEvent(.finishedChangingRoles)
}
}
private func presentChangePermissionsScreen(permissions: RoomPermissions, group: RoomRolesAndPermissionsScreenPermissionsGroup) {
let parameters = RoomChangePermissionsScreenCoordinatorParameters(permissions: permissions,
permissionsGroup: group,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
analytics: analytics)
let coordinator = RoomChangePermissionsScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
// When discarding changes is finalised, either use an event or remove this action.
navigationStackCoordinator.pop()
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator) { [stateMachine] in
stateMachine.tryEvent(.finishedChangingPermissions)
}
}
}