From 2fdc37ce7238f753edea9da8dc5a6fbc01f94444 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 21 Jan 2026 18:59:29 +0100 Subject: [PATCH] implemented the flow to create room in a selected space --- .../en-US.lproj/Localizable.strings | 2 +- .../en.lproj/Localizable.strings | 2 +- .../SpaceFlowCoordinator.swift | 65 ++++++++++++++++++- .../SpacesTabFlowCoordinator.swift | 2 +- .../StartChatFlowCoordinator.swift | 41 +++++++++--- ElementX/Sources/Generated/Strings.swift | 2 +- .../SpaceScreen/SpaceScreenCoordinator.swift | 3 + .../SpaceScreen/SpaceScreenModels.swift | 3 + .../SpaceScreen/SpaceScreenViewModel.swift | 16 +++++ .../Spaces/SpaceScreen/View/SpaceScreen.swift | 5 ++ 10 files changed, 126 insertions(+), 15 deletions(-) diff --git a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings index dab73e8ba..74e277814 100644 --- a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings @@ -67,7 +67,7 @@ "action_copy_link_to_message" = "Copy link to message"; "action_copy_text" = "Copy text"; "action_create" = "Create"; -"action_create_a_room" = "Create a room"; +"action_create_a_room" = "Create room"; "action_create_space" = "Create space"; "action_deactivate" = "Deactivate"; "action_deactivate_account" = "Deactivate account"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 71c924cd9..02e158e1f 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -67,7 +67,7 @@ "action_copy_link_to_message" = "Copy link to message"; "action_copy_text" = "Copy text"; "action_create" = "Create"; -"action_create_a_room" = "Create a room"; +"action_create_a_room" = "Create room"; "action_create_space" = "Create space"; "action_deactivate" = "Deactivate"; "action_deactivate_account" = "Deactivate account"; diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index eb67ab559..cd578a4ad 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -44,6 +44,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { private var membersFlowCoordinator: RoomMembersFlowCoordinator? private var settingsFlowCoordinator: SpaceSettingsFlowCoordinator? private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator? + private var createChildRoomFlowCoordinator: StartChatFlowCoordinator? indirect enum State: StateType { /// The state machine hasn't started. @@ -65,6 +66,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case rolesAndPermissionsFlow + case createChildRoomFlow + case leftSpace } @@ -102,6 +105,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case startRolesAndPermissionsFlow case stopRolesAndPermissionsFlow + + case startCreateChildRoomFlow + case stopCreateChildRoomFlow } private let stateMachine: StateMachine @@ -170,12 +176,15 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case .rolesAndPermissionsFlow: rolesAndPermissionsFlowCoordinator?.clearRoute(animated: animated) 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 - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next function_body_length cyclomatic_complexity private func configureStateMachine() { stateMachine.addRoutes(event: .start, transitions: [.initial => .space]) { [weak self] _ in self?.presentSpace() @@ -288,6 +297,21 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { 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 fatalError("Unexpected transition: \(context)") } @@ -323,6 +347,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.startRolesAndPermissionsFlow, userInfo: roomProxy) case .addExistingChildren: stateMachine.tryEvent(.addRooms) + case .displayCreateChildRoomFlow(let space): + stateMachine.tryEvent(.startCreateChildRoomFlow, userInfo: space) } } .store(in: &cancellables) @@ -534,4 +560,41 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { rolesAndPermissionsFlowCoordinator = flowCoordinator 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() + } } diff --git a/ElementX/Sources/FlowCoordinators/SpacesTabFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpacesTabFlowCoordinator.swift index de88d065a..d382ad779 100644 --- a/ElementX/Sources/FlowCoordinators/SpacesTabFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpacesTabFlowCoordinator.swift @@ -225,7 +225,7 @@ class SpacesTabFlowCoordinator: FlowCoordinatorProtocol { .store(in: &cancellables) navigationSplitCoordinator.setSheetCoordinator(coordinator) { [weak self] in - self?.stateMachine.tryEvent(.dismissedCreateSpaceFlow, userInfo: spaceRoomListProxy) + self?.stateMachine.tryEvent(.dismissedCreateSpaceFlow) } flowCoordinator.start(animated: true) diff --git a/ElementX/Sources/FlowCoordinators/StartChatFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/StartChatFlowCoordinator.swift index 1692a6187..234313cf2 100644 --- a/ElementX/Sources/FlowCoordinators/StartChatFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/StartChatFlowCoordinator.swift @@ -25,6 +25,7 @@ enum StartChatFlowCoordinatorAction { enum StartChatFlowCoordinatorEntryPoint { case startChat case createSpace + case createRoomInSpace(SpaceServiceRoomProtocol) } class StartChatFlowCoordinator: FlowCoordinatorProtocol { @@ -57,7 +58,7 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol { enum Event: EventType { /// The flow is being started. - case start(entryPoint: StartChatFlowCoordinatorEntryPoint) + case start /// The user would like to create a room. case createRoom @@ -96,7 +97,7 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol { } func start(animated: Bool) { - stateMachine.tryEvent(.start(entryPoint: entryPoint)) + stateMachine.tryEvent(.start) } func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { @@ -126,12 +127,24 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol { // MARK: - Private private func configureStateMachine() { - stateMachine.addRoutes(event: .start(entryPoint: .startChat), transitions: [.initial => .startChat]) { [weak self] _ in - self?.presentStartChatScreen() - } - - stateMachine.addRoutes(event: .start(entryPoint: .createSpace), transitions: [.initial => .createRoom]) { [weak self] _ in - self?.presentCreateRoomScreen(isSpace: true, isRoot: true) + stateMachine.addRouteMapping { [weak self] event, fromState, _ in + guard let self, event == .start, fromState == .initial else { return nil } + switch entryPoint { + case .startChat: + return .startChat + 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 @@ -191,9 +204,17 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol { 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, - spaceSelectionMode: !isSpace && flowParameters.appSettings.createSpaceEnabled ? .list : nil, + spaceSelectionMode: spaceSelectionMode, shouldShowCancelButton: isRoot, userSession: flowParameters.userSession, userIndicatorController: flowParameters.userIndicatorController, diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 76ff00fe8..f7fde0518 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -168,7 +168,7 @@ internal enum L10n { internal static var actionCopyText: String { return L10n.tr("Localizable", "action_copy_text") } /// 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") } /// Create space internal static var actionCreateSpace: String { return L10n.tr("Localizable", "action_create_space") } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift index 393de74cc..f72e41bbf 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -29,6 +29,7 @@ enum SpaceScreenCoordinatorAction { case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) case displayRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol) case addExistingChildren + case displayCreateChildRoomFlow(space: SpaceServiceRoomProtocol) } final class SpaceScreenCoordinator: CoordinatorProtocol { @@ -75,6 +76,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.displayRolesAndPermissions(roomProxy: roomProxy)) case .addExistingChildren: actionsSubject.send(.addExistingChildren) + case .displayCreateChildRoomFlow(let space): + actionsSubject.send(.displayCreateChildRoomFlow(space: space)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index 69fff17aa..63c977267 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -17,6 +17,7 @@ enum SpaceScreenViewModelAction { case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) case addExistingChildren + case displayCreateChildRoomFlow(space: SpaceServiceRoomProtocol) } struct SpaceScreenViewState: BindableState { @@ -34,6 +35,7 @@ struct SpaceScreenViewState: BindableState { var canEditRolesAndPermissions = false var canEditSecurityAndPrivacy = false var canEditChildren = false + var canCreateRoom = false var editMode: EditMode = .inactive var editModeSelectedIDs: Set = [] @@ -72,6 +74,7 @@ enum SpaceScreenViewAction { case spaceSettings(roomProxy: JoinedRoomProxyProtocol) case displayMembers(roomProxy: JoinedRoomProxyProtocol) case addExistingRooms + case createChildRoom case manageChildren case removeSelectedChildren case confirmRemoveSelectedChildren diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index be96271a6..fd068c2e9 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -75,6 +75,10 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc .weakAssign(to: \.state.selectedSpaceRoomID, on: self) .store(in: &cancellables) + appSettings.$createSpaceEnabled + .weakAssign(to: \.state.canCreateRoom, on: self) + .store(in: &cancellables) + Task { if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id) { // Required to listen for membership updates in the members flow @@ -155,6 +159,8 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc } completion: { self.state.editModeSelectedIDs.removeAll() } + case .createChildRoom: + Task { await createChildRoom() } } } @@ -165,6 +171,16 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc // 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 { state.joiningRoomIDs.insert(spaceServiceRoom.id) defer { state.joiningRoomIDs.remove(spaceServiceRoom.id) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift index 390c6c827..342143594 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift @@ -121,6 +121,11 @@ struct SpaceScreen: View { Menu { if context.viewState.canEditChildren { Section { + if context.viewState.canCreateRoom { + Button { context.send(viewAction: .createChildRoom) } label: { + Label(L10n.actionCreateARoom, icon: \.plus) + } + } Button { context.send(viewAction: .addExistingRooms) } label: { Label(L10n.actionAddExistingRooms, icon: \.room) }