implemented the flow to create room in a selected space

This commit is contained in:
Mauro Romito
2026-01-21 18:59:29 +01:00
committed by Mauro
parent 8b38c2874a
commit d012380b01
10 changed files with 126 additions and 15 deletions
@@ -67,7 +67,7 @@
"action_copy_link_to_message" = "Copy link to message"; "action_copy_link_to_message" = "Copy link to message";
"action_copy_text" = "Copy text"; "action_copy_text" = "Copy text";
"action_create" = "Create"; "action_create" = "Create";
"action_create_a_room" = "Create a room"; "action_create_a_room" = "Create room";
"action_create_space" = "Create space"; "action_create_space" = "Create space";
"action_deactivate" = "Deactivate"; "action_deactivate" = "Deactivate";
"action_deactivate_account" = "Deactivate account"; "action_deactivate_account" = "Deactivate account";
@@ -67,7 +67,7 @@
"action_copy_link_to_message" = "Copy link to message"; "action_copy_link_to_message" = "Copy link to message";
"action_copy_text" = "Copy text"; "action_copy_text" = "Copy text";
"action_create" = "Create"; "action_create" = "Create";
"action_create_a_room" = "Create a room"; "action_create_a_room" = "Create room";
"action_create_space" = "Create space"; "action_create_space" = "Create space";
"action_deactivate" = "Deactivate"; "action_deactivate" = "Deactivate";
"action_deactivate_account" = "Deactivate account"; "action_deactivate_account" = "Deactivate account";
@@ -44,6 +44,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
private var membersFlowCoordinator: RoomMembersFlowCoordinator? private var membersFlowCoordinator: RoomMembersFlowCoordinator?
private var settingsFlowCoordinator: SpaceSettingsFlowCoordinator? private var settingsFlowCoordinator: SpaceSettingsFlowCoordinator?
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator? private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
private var createChildRoomFlowCoordinator: StartChatFlowCoordinator?
indirect enum State: StateType { indirect enum State: StateType {
/// The state machine hasn't started. /// The state machine hasn't started.
@@ -65,6 +66,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
case rolesAndPermissionsFlow case rolesAndPermissionsFlow
case createChildRoomFlow
case leftSpace case leftSpace
} }
@@ -102,6 +105,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
case startRolesAndPermissionsFlow case startRolesAndPermissionsFlow
case stopRolesAndPermissionsFlow case stopRolesAndPermissionsFlow
case startCreateChildRoomFlow
case stopCreateChildRoomFlow
} }
private let stateMachine: StateMachine<State, Event> private let stateMachine: StateMachine<State, Event>
@@ -170,12 +176,15 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
case .rolesAndPermissionsFlow: case .rolesAndPermissionsFlow:
rolesAndPermissionsFlowCoordinator?.clearRoute(animated: animated) rolesAndPermissionsFlowCoordinator?.clearRoute(animated: animated)
clearRoute(animated: animated) // Re-run with the state machine back in the .space state. clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
case .createChildRoomFlow:
createChildRoomFlowCoordinator?.clearRoute(animated: animated)
clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
} }
} }
// MARK: - Private // MARK: - Private
// swiftlint:disable:next cyclomatic_complexity // swiftlint:disable:next function_body_length cyclomatic_complexity
private func configureStateMachine() { private func configureStateMachine() {
stateMachine.addRoutes(event: .start, transitions: [.initial => .space]) { [weak self] _ in stateMachine.addRoutes(event: .start, transitions: [.initial => .space]) { [weak self] _ in
self?.presentSpace() self?.presentSpace()
@@ -288,6 +297,21 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
rolesAndPermissionsFlowCoordinator = nil rolesAndPermissionsFlowCoordinator = nil
} }
stateMachine.addRouteMapping { event, fromState, _ in
guard event == .startCreateChildRoomFlow, case .space = fromState else { return nil }
return .createChildRoomFlow
} handler: { [weak self] context in
guard let space = context.userInfo as? SpaceServiceRoomProtocol else { fatalError("The space is missing") }
self?.startCreateChildFlow(space: space)
}
stateMachine.addRouteMapping { event, fromState, _ in
guard event == .stopCreateChildRoomFlow, case .createChildRoomFlow = fromState else { return nil }
return .space
} handler: { [weak self] _ in
self?.createChildRoomFlowCoordinator = nil
}
stateMachine.addErrorHandler { context in stateMachine.addErrorHandler { context in
fatalError("Unexpected transition: \(context)") fatalError("Unexpected transition: \(context)")
} }
@@ -323,6 +347,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.startRolesAndPermissionsFlow, userInfo: roomProxy) stateMachine.tryEvent(.startRolesAndPermissionsFlow, userInfo: roomProxy)
case .addExistingChildren: case .addExistingChildren:
stateMachine.tryEvent(.addRooms) stateMachine.tryEvent(.addRooms)
case .displayCreateChildRoomFlow(let space):
stateMachine.tryEvent(.startCreateChildRoomFlow, userInfo: space)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -534,4 +560,41 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
rolesAndPermissionsFlowCoordinator = flowCoordinator rolesAndPermissionsFlowCoordinator = flowCoordinator
flowCoordinator.start() flowCoordinator.start()
} }
private func startCreateChildFlow(space: SpaceServiceRoomProtocol) {
let stackCoordinator = NavigationStackCoordinator()
let flowCoordinator = StartChatFlowCoordinator(entryPoint: .createRoomInSpace(space),
userDiscoveryService: UserDiscoveryService(clientProxy: flowParameters.userSession.clientProxy),
navigationStackCoordinator: stackCoordinator,
flowParameters: flowParameters)
var flowCoordinatorResult: StartChatFlowCoordinatorAction.Result?
flowCoordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .finished(let result):
flowCoordinatorResult = result
navigationStackCoordinator.setSheetCoordinator(nil)
case .showRoomDirectory:
fatalError("Not implemented yet")
}
}
.store(in: &cancellables)
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
guard let self else { return }
stateMachine.tryEvent(.stopCreateChildRoomFlow)
switch flowCoordinatorResult {
case .room(let id):
stateMachine.tryEvent(.startRoomFlow(roomID: id))
case .space(let spaceRoomListProxy):
stateMachine.tryEvent(.startChildFlow, userInfo: spaceRoomListProxy)
case .cancelled, .none:
break
}
}
createChildRoomFlowCoordinator = flowCoordinator
flowCoordinator.start()
}
} }
@@ -225,7 +225,7 @@ class SpacesTabFlowCoordinator: FlowCoordinatorProtocol {
.store(in: &cancellables) .store(in: &cancellables)
navigationSplitCoordinator.setSheetCoordinator(coordinator) { [weak self] in navigationSplitCoordinator.setSheetCoordinator(coordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissedCreateSpaceFlow, userInfo: spaceRoomListProxy) self?.stateMachine.tryEvent(.dismissedCreateSpaceFlow)
} }
flowCoordinator.start(animated: true) flowCoordinator.start(animated: true)
@@ -25,6 +25,7 @@ enum StartChatFlowCoordinatorAction {
enum StartChatFlowCoordinatorEntryPoint { enum StartChatFlowCoordinatorEntryPoint {
case startChat case startChat
case createSpace case createSpace
case createRoomInSpace(SpaceServiceRoomProtocol)
} }
class StartChatFlowCoordinator: FlowCoordinatorProtocol { class StartChatFlowCoordinator: FlowCoordinatorProtocol {
@@ -57,7 +58,7 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol {
enum Event: EventType { enum Event: EventType {
/// The flow is being started. /// The flow is being started.
case start(entryPoint: StartChatFlowCoordinatorEntryPoint) case start
/// The user would like to create a room. /// The user would like to create a room.
case createRoom case createRoom
@@ -96,7 +97,7 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol {
} }
func start(animated: Bool) { func start(animated: Bool) {
stateMachine.tryEvent(.start(entryPoint: entryPoint)) stateMachine.tryEvent(.start)
} }
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
@@ -126,12 +127,24 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol {
// MARK: - Private // MARK: - Private
private func configureStateMachine() { private func configureStateMachine() {
stateMachine.addRoutes(event: .start(entryPoint: .startChat), transitions: [.initial => .startChat]) { [weak self] _ in stateMachine.addRouteMapping { [weak self] event, fromState, _ in
self?.presentStartChatScreen() guard let self, event == .start, fromState == .initial else { return nil }
} switch entryPoint {
case .startChat:
stateMachine.addRoutes(event: .start(entryPoint: .createSpace), transitions: [.initial => .createRoom]) { [weak self] _ in return .startChat
self?.presentCreateRoomScreen(isSpace: true, isRoot: true) case .createSpace, .createRoomInSpace:
return .createRoom
}
} handler: { [weak self] _ in
guard let self else { return }
switch entryPoint {
case .startChat:
presentStartChatScreen()
case .createSpace:
presentCreateRoomScreen(isSpace: true, isRoot: true)
case .createRoomInSpace(let space):
presentCreateRoomScreen(isSpace: false, selectedSpace: space, isRoot: true)
}
} }
stateMachine.addRoutes(event: .createRoom, transitions: [.startChat => .createRoom]) { [weak self] _ in stateMachine.addRoutes(event: .createRoom, transitions: [.startChat => .createRoom]) { [weak self] _ in
@@ -191,9 +204,17 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
} }
private func presentCreateRoomScreen(isSpace: Bool, isRoot: Bool) { private func presentCreateRoomScreen(isSpace: Bool, selectedSpace: SpaceServiceRoomProtocol? = nil, isRoot: Bool) {
let spaceSelectionMode: CreateRoomScreenSpaceSelectionMode? = if let selectedSpace {
.selected(selectedSpace)
} else if !isSpace, flowParameters.appSettings.createSpaceEnabled {
.list
} else {
nil
}
let createParameters = CreateRoomScreenCoordinatorParameters(isSpace: isSpace, let createParameters = CreateRoomScreenCoordinatorParameters(isSpace: isSpace,
spaceSelectionMode: !isSpace && flowParameters.appSettings.createSpaceEnabled ? .list : nil, spaceSelectionMode: spaceSelectionMode,
shouldShowCancelButton: isRoot, shouldShowCancelButton: isRoot,
userSession: flowParameters.userSession, userSession: flowParameters.userSession,
userIndicatorController: flowParameters.userIndicatorController, userIndicatorController: flowParameters.userIndicatorController,
+1 -1
View File
@@ -168,7 +168,7 @@ internal enum L10n {
internal static var actionCopyText: String { return L10n.tr("Localizable", "action_copy_text") } internal static var actionCopyText: String { return L10n.tr("Localizable", "action_copy_text") }
/// Create /// Create
internal static var actionCreate: String { return L10n.tr("Localizable", "action_create") } internal static var actionCreate: String { return L10n.tr("Localizable", "action_create") }
/// Create a room /// Create room
internal static var actionCreateARoom: String { return L10n.tr("Localizable", "action_create_a_room") } internal static var actionCreateARoom: String { return L10n.tr("Localizable", "action_create_a_room") }
/// Create space /// Create space
internal static var actionCreateSpace: String { return L10n.tr("Localizable", "action_create_space") } internal static var actionCreateSpace: String { return L10n.tr("Localizable", "action_create_space") }
@@ -29,6 +29,7 @@ enum SpaceScreenCoordinatorAction {
case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol)
case displayRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol) case displayRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol)
case addExistingChildren case addExistingChildren
case displayCreateChildRoomFlow(space: SpaceServiceRoomProtocol)
} }
final class SpaceScreenCoordinator: CoordinatorProtocol { final class SpaceScreenCoordinator: CoordinatorProtocol {
@@ -75,6 +76,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.displayRolesAndPermissions(roomProxy: roomProxy)) actionsSubject.send(.displayRolesAndPermissions(roomProxy: roomProxy))
case .addExistingChildren: case .addExistingChildren:
actionsSubject.send(.addExistingChildren) actionsSubject.send(.addExistingChildren)
case .displayCreateChildRoomFlow(let space):
actionsSubject.send(.displayCreateChildRoomFlow(space: space))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -17,6 +17,7 @@ enum SpaceScreenViewModelAction {
case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displayMembers(roomProxy: JoinedRoomProxyProtocol)
case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol)
case addExistingChildren case addExistingChildren
case displayCreateChildRoomFlow(space: SpaceServiceRoomProtocol)
} }
struct SpaceScreenViewState: BindableState { struct SpaceScreenViewState: BindableState {
@@ -34,6 +35,7 @@ struct SpaceScreenViewState: BindableState {
var canEditRolesAndPermissions = false var canEditRolesAndPermissions = false
var canEditSecurityAndPrivacy = false var canEditSecurityAndPrivacy = false
var canEditChildren = false var canEditChildren = false
var canCreateRoom = false
var editMode: EditMode = .inactive var editMode: EditMode = .inactive
var editModeSelectedIDs: Set<String> = [] var editModeSelectedIDs: Set<String> = []
@@ -72,6 +74,7 @@ enum SpaceScreenViewAction {
case spaceSettings(roomProxy: JoinedRoomProxyProtocol) case spaceSettings(roomProxy: JoinedRoomProxyProtocol)
case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displayMembers(roomProxy: JoinedRoomProxyProtocol)
case addExistingRooms case addExistingRooms
case createChildRoom
case manageChildren case manageChildren
case removeSelectedChildren case removeSelectedChildren
case confirmRemoveSelectedChildren case confirmRemoveSelectedChildren
@@ -75,6 +75,10 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
.weakAssign(to: \.state.selectedSpaceRoomID, on: self) .weakAssign(to: \.state.selectedSpaceRoomID, on: self)
.store(in: &cancellables) .store(in: &cancellables)
appSettings.$createSpaceEnabled
.weakAssign(to: \.state.canCreateRoom, on: self)
.store(in: &cancellables)
Task { Task {
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id) { if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id) {
// Required to listen for membership updates in the members flow // Required to listen for membership updates in the members flow
@@ -155,6 +159,8 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
} completion: { } completion: {
self.state.editModeSelectedIDs.removeAll() self.state.editModeSelectedIDs.removeAll()
} }
case .createChildRoom:
Task { await createChildRoom() }
} }
} }
@@ -165,6 +171,16 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
// MARK: - Private // MARK: - Private
private func createChildRoom() async {
switch await spaceServiceProxy.spaceForIdentifier(spaceID: spaceRoomListProxy.id) {
case .success(.some(let space)):
actionsSubject.send(.displayCreateChildRoomFlow(space: space))
default:
MXLog.error("Unable to create child room: space not found")
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
private func join(_ spaceServiceRoom: SpaceServiceRoomProtocol) async { private func join(_ spaceServiceRoom: SpaceServiceRoomProtocol) async {
state.joiningRoomIDs.insert(spaceServiceRoom.id) state.joiningRoomIDs.insert(spaceServiceRoom.id)
defer { state.joiningRoomIDs.remove(spaceServiceRoom.id) } defer { state.joiningRoomIDs.remove(spaceServiceRoom.id) }
@@ -121,6 +121,11 @@ struct SpaceScreen: View {
Menu { Menu {
if context.viewState.canEditChildren { if context.viewState.canEditChildren {
Section { Section {
if context.viewState.canCreateRoom {
Button { context.send(viewAction: .createChildRoom) } label: {
Label(L10n.actionCreateARoom, icon: \.plus)
}
}
Button { context.send(viewAction: .addExistingRooms) } label: { Button { context.send(viewAction: .addExistingRooms) } label: {
Label(L10n.actionAddExistingRooms, icon: \.room) Label(L10n.actionAddExistingRooms, icon: \.room)
} }