From cddb4d4053aa0a94567ee61b2d51baf2793676c2 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Wed, 5 Nov 2025 18:07:44 +0100 Subject: [PATCH] Space Settings - Navigations (#4691) * Implementation for all navigations inside the space settings aside the left space action * pr suggestions --- .../SpaceFlowCoordinator.swift | 4 + .../SpaceSettingsFlowCoordinator.swift | 219 +++++++++++++++++- .../SwiftUI/Views/LoadableAvatarImage.swift | 40 +++- .../Views/OverridableAvatarImage.swift | 4 +- .../RoomDetailsEditScreenModels.swift | 1 + .../RoomDetailsEditScreenViewModel.swift | 2 + .../View/RoomDetailsEditScreen.swift | 1 + .../View/SecurityAndPrivacyScreen.swift | 11 +- .../View/UserDetailsEditScreen.swift | 1 + .../SpaceSettingsScreenCoordinator.swift | 21 +- 10 files changed, 287 insertions(+), 17 deletions(-) diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index e3a0a856c..e66495b73 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -432,6 +432,10 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { switch actions { case .finished: stateMachine.tryEvent(.stopSettingsFlow) + case .presentCallScreen(let roomProxy): + actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) + case .verifyUser(userID: let userID): + actionsSubject.send(.verifyUser(userID: userID)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift index ce90b80fe..19bc13ebc 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift @@ -11,6 +11,8 @@ import SwiftState enum SpaceSettingsFlowCoordinatorAction { case finished + case presentCallScreen(roomProxy: JoinedRoomProxyProtocol) + case verifyUser(userID: String) } final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { @@ -19,12 +21,40 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { case initial /// The space settings screen case spaceSettings + /// The edit details screen presented modally + case editDetailsScreen + /// The security and privacy screen + case securityAndPrivacy + /// The edit address screen + case editAddress + + // Other flows + /// The roles and permissions screen + case rolesAndPermissionsFlow + /// The members flow screen + case membersFlow } enum Event: EventType { case start case presentSpaceSettings + + case presentEditDetailsScreen + case dismissedEditDetailsScreen + + case presentSecurityAndPrivacyScreen + case dismissedSecurityAndPrivacyScreen + + case presentEditAddress + case dismissedEditAddress + + // Other flows + case startMembersListFlow + case stopMembersListFlow + + case startRolesAndPermissionsFlow + case stopRolesAndPermissionsFlow } private let roomProxy: JoinedRoomProxyProtocol @@ -34,6 +64,9 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { private let stateMachine: StateMachine private var cancellables = Set() + private var membersFlowCoordinator: RoomMembersFlowCoordinator? + private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator? + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -63,7 +96,19 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { case .initial: break case .spaceSettings: - navigationStackCoordinator.pop(animated: animated) // SpaceSettingsScreen + navigationStackCoordinator.pop(animated: animated) + case .securityAndPrivacy: + navigationStackCoordinator.pop(animated: animated) + clearRoute(animated: animated) + case .editDetailsScreen, .editAddress: + navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) + clearRoute(animated: animated) + case .rolesAndPermissionsFlow: + rolesAndPermissionsFlowCoordinator?.clearRoute(animated: animated) + clearRoute(animated: animated) + case .membersFlow: + membersFlowCoordinator?.clearRoute(animated: animated) + clearRoute(animated: animated) } } @@ -73,6 +118,31 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { case (.initial, .presentSpaceSettings): return .spaceSettings + case (.spaceSettings, .presentEditDetailsScreen): + return .editDetailsScreen + case (.editDetailsScreen, .dismissedEditDetailsScreen): + return .spaceSettings + + case (.spaceSettings, .presentSecurityAndPrivacyScreen): + return .securityAndPrivacy + case (.securityAndPrivacy, .dismissedSecurityAndPrivacyScreen): + return .spaceSettings + + case (.securityAndPrivacy, .presentEditAddress): + return .editAddress + case (.editAddress, .dismissedEditAddress): + return .securityAndPrivacy + + case (.spaceSettings, .startMembersListFlow): + return .membersFlow + case (.membersFlow, .stopMembersListFlow): + return .spaceSettings + + case (.spaceSettings, .startRolesAndPermissionsFlow): + return .rolesAndPermissionsFlow + case (.rolesAndPermissionsFlow, .stopRolesAndPermissionsFlow): + return .spaceSettings + default: return nil } @@ -85,6 +155,32 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { case (.initial, .presentSpaceSettings, .spaceSettings): presentSpaceSettings(animated: animated) + case (.spaceSettings, .presentEditDetailsScreen, .editDetailsScreen): + presentEditDetailsScreen() + + case (.editDetailsScreen, .dismissedEditDetailsScreen, .spaceSettings): + break + + case (.spaceSettings, .presentSecurityAndPrivacyScreen, .securityAndPrivacy): + presentSecurityAndPrivacyScreen() + case (.securityAndPrivacy, .dismissedSecurityAndPrivacyScreen, .spaceSettings): + break + + case (.securityAndPrivacy, .presentEditAddress, .editAddress): + presentEditAddressScreen() + case (.editAddress, .dismissedEditAddress, .securityAndPrivacy): + break + + case (.spaceSettings, .startMembersListFlow, .membersFlow): + startMembersListFlow() + case (.membersFlow, .stopMembersListFlow, .spaceSettings): + membersFlowCoordinator = nil + + case (.spaceSettings, .startRolesAndPermissionsFlow, .rolesAndPermissionsFlow): + startRolesAndPermissionsFlow() + case (.rolesAndPermissionsFlow, .stopRolesAndPermissionsFlow, .spaceSettings): + rolesAndPermissionsFlowCoordinator = nil + default: fatalError("Unhandled transition") } @@ -101,7 +197,17 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { appSettings: flowParameters.appSettings)) coordinator.actionsPublisher.sink { [weak self] action in - switch action { } + guard let self else { return } + switch action { + case .presentEditDetailsScreen: + stateMachine.tryEvent(.presentEditDetailsScreen) + case .presentSecurityAndPrivacyScreen: + stateMachine.tryEvent(.presentSecurityAndPrivacyScreen) + case .presentMembersListScreen: + stateMachine.tryEvent(.startMembersListFlow) + case .presentRolesAndPermissionsScreen: + stateMachine.tryEvent(.startRolesAndPermissionsFlow) + } } .store(in: &cancellables) @@ -109,4 +215,113 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { self?.actionsSubject.send(.finished) } } + + private func presentEditDetailsScreen() { + let stackCoordinator = NavigationStackCoordinator() + let parameters = RoomDetailsEditScreenCoordinatorParameters(roomProxy: roomProxy, + userSession: flowParameters.userSession, + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: flowParameters.appSettings), + navigationStackCoordinator: stackCoordinator, + userIndicatorController: flowParameters.userIndicatorController, + orientationManager: flowParameters.appMediator.windowManager, + appSettings: flowParameters.appSettings) + + let coordinator = RoomDetailsEditScreenCoordinator(parameters: parameters) + + coordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss: + navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + stackCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissedEditDetailsScreen) + } + } + + private func presentSecurityAndPrivacyScreen() { + let coordinator = SecurityAndPrivacyScreenCoordinator(parameters: .init(roomProxy: roomProxy, + clientProxy: flowParameters.userSession.clientProxy, + userIndicatorController: flowParameters.userIndicatorController)) + + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .displayEditAddressScreen: + self.stateMachine.tryEvent(.presentEditAddress) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.push(coordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissedSecurityAndPrivacyScreen) + } + } + + private func presentEditAddressScreen() { + let stackCoordinator = NavigationStackCoordinator() + let coordinator = EditRoomAddressScreenCoordinator(parameters: .init(roomProxy: roomProxy, + clientProxy: flowParameters.userSession.clientProxy, + userIndicatorController: flowParameters.userIndicatorController)) + + coordinator.actionsPublisher.sink { [weak self] action in + switch action { + case .dismiss: + self?.navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + stackCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissedEditAddress) + } + } + + // MARK: - Other flows + + private func startRolesAndPermissionsFlow() { + let parameters = RoomRolesAndPermissionsFlowCoordinatorParameters(roomProxy: roomProxy, + mediaProvider: flowParameters.userSession.mediaProvider, + navigationStackCoordinator: navigationStackCoordinator, + userIndicatorController: flowParameters.userIndicatorController, + analytics: flowParameters.analytics) + let coordinator = RoomRolesAndPermissionsFlowCoordinator(parameters: parameters) + coordinator.actionsPublisher.sink { [weak self] action in + switch action { + case .complete: + self?.stateMachine.tryEvent(.stopRolesAndPermissionsFlow) + } + } + .store(in: &cancellables) + + rolesAndPermissionsFlowCoordinator = coordinator + coordinator.start() + } + + private func startMembersListFlow() { + let flowCoordinator = RoomMembersFlowCoordinator(entryPoint: .roomMembersList, + roomProxy: roomProxy, + navigationStackCoordinator: navigationStackCoordinator, + flowParameters: flowParameters) + flowCoordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .finished: + stateMachine.tryEvent(.stopMembersListFlow) + case .presentCallScreen(let roomProxy): + actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) + case .verifyUser(let userID): + actionsSubject.send(.verifyUser(userID: userID)) + } + } + .store(in: &cancellables) + + membersFlowCoordinator = flowCoordinator + flowCoordinator.start(animated: true) + } } diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift index 478bb2d36..3c7ec8795 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift @@ -54,14 +54,10 @@ struct LoadableAvatarImage: View { avatar .frame(width: frameSize, height: frameSize) .background(Color.compound.bgCanvasDefault) - .clipShape(avatarShape) + .clipAvatar(isSpace: isSpace, scaledSize: _frameSize) .environment(\.shouldAutomaticallyLoadImages, true) // We always load avatars. } - private var avatarShape: some Shape { - isSpace ? AnyShape(RoundedRectangle(cornerRadius: frameSize / 4)) : AnyShape(Circle()) - } - @ViewBuilder private var avatar: some View { if let url { @@ -79,3 +75,37 @@ struct LoadableAvatarImage: View { } } } + +extension View { + func clipAvatar(isSpace: Bool, size: CGFloat) -> some View { + modifier(ClipAvatarModifier(isSpace: isSpace, size: size)) + } + + func clipAvatar(isSpace: Bool, scaledSize: ScaledMetric) -> some View { + modifier(ClipAvatarModifier(isSpace: isSpace, scaledSize: scaledSize)) + } +} + +struct ClipAvatarModifier: ViewModifier { + private let isSpace: Bool + @ScaledMetric private var scaledSize: CGFloat + + init(isSpace: Bool, size: CGFloat) { + self.isSpace = isSpace + _scaledSize = ScaledMetric(wrappedValue: size) + } + + init(isSpace: Bool, scaledSize: ScaledMetric) { + self.isSpace = isSpace + _scaledSize = scaledSize + } + + func body(content: Content) -> some View { + content + .clipShape(avatarShape) + } + + private var avatarShape: some Shape { + isSpace ? AnyShape(RoundedRectangle(cornerRadius: scaledSize / 4)) : AnyShape(Circle()) + } +} diff --git a/ElementX/Sources/Other/SwiftUI/Views/OverridableAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/OverridableAvatarImage.swift index cdfd65e46..30f9d7795 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/OverridableAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/OverridableAvatarImage.swift @@ -14,6 +14,7 @@ struct OverridableAvatarImage: View { let url: URL? let name: String? let contentID: String + let isSpace: Bool let avatarSize: Avatars.Size let mediaProvider: MediaProviderProtocol? @@ -27,11 +28,12 @@ struct OverridableAvatarImage: View { ProgressView() } .scaledFrame(size: avatarSize.value) - .clipShape(Circle()) + .clipAvatar(isSpace: isSpace, size: avatarSize.value) } else { LoadableAvatarImage(url: url, name: name, contentID: contentID, + isSpace: isSpace, avatarSize: avatarSize, mediaProvider: mediaProvider) } diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift index 238532dba..e89de0d6d 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift @@ -23,6 +23,7 @@ struct RoomDetailsEditScreenViewStateBindings { struct RoomDetailsEditScreenViewState: BindableState { let roomID: String + let isSpace: Bool let initialAvatarURL: URL? let initialName: String let initialTopic: String diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift index 76e834d7a..f0fa51a7b 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift @@ -34,8 +34,10 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe let roomAvatar = roomProxy.infoPublisher.value.avatarURL let roomName = roomProxy.infoPublisher.value.displayName let roomTopic = roomProxy.infoPublisher.value.topic + let isSpace = roomProxy.infoPublisher.value.isSpace super.init(initialViewState: RoomDetailsEditScreenViewState(roomID: roomProxy.id, + isSpace: isSpace, initialAvatarURL: roomAvatar, initialName: roomName ?? "", initialTopic: roomTopic ?? "", diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift index a7babc6e0..4c1b250c0 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift @@ -59,6 +59,7 @@ struct RoomDetailsEditScreen: View { url: context.viewState.avatarURL, name: context.viewState.initialName, contentID: context.viewState.roomID, + isSpace: context.viewState.isSpace, avatarSize: .user(on: .memberDetails), mediaProvider: context.mediaProvider) .accessibilityLabel(L10n.a11yEditAvatar) diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/View/SecurityAndPrivacyScreen.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/View/SecurityAndPrivacyScreen.swift index d087b9e75..b522aca30 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/View/SecurityAndPrivacyScreen.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/View/SecurityAndPrivacyScreen.swift @@ -142,9 +142,14 @@ struct SecurityAndPrivacyScreen: View { private var addAddressSection: some View { Section { ListRow(kind: .custom { - Button(L10n.screenSecurityAndPrivacyAddRoomAddressAction) { context.send(viewAction: .editAddress) } - .foregroundColor(.compound.textActionAccent) - .padding(ListRowPadding.insets) + Button { + context.send(viewAction: .editAddress) + } label: { + Text(L10n.screenSecurityAndPrivacyAddRoomAddressAction) + .foregroundColor(.compound.textActionAccent) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(ListRowPadding.insets) }) } } diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift index 9a145802d..f924d3568 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift @@ -55,6 +55,7 @@ struct UserDetailsEditScreen: View { url: context.viewState.selectedAvatarURL, name: context.viewState.currentDisplayName, contentID: context.viewState.userID, + isSpace: false, avatarSize: .user(on: .editUserDetails), mediaProvider: context.mediaProvider) .overlay(alignment: .bottomTrailing) { diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift index 4cdcf3c83..3b42d693b 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift @@ -18,7 +18,12 @@ struct SpaceSettingsScreenCoordinatorParameters { let appSettings: AppSettings } -enum SpaceSettingsScreenCoordinatorAction { } +enum SpaceSettingsScreenCoordinatorAction { + case presentEditDetailsScreen + case presentSecurityAndPrivacyScreen + case presentMembersListScreen + case presentRolesAndPermissionsScreen +} final class SpaceSettingsScreenCoordinator: CoordinatorProtocol { private let viewModel: RoomDetailsScreenViewModelProtocol @@ -46,15 +51,19 @@ final class SpaceSettingsScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .requestNotificationSettingsPresentation, .requestRecipientDetailsPresentation, .requestInvitePeoplePresentation, .leftRoom, .requestPollsHistoryPresentation, .requestRolesAndPermissionsPresentation, .startCall, .displayPinnedEventsTimeline, .displayMediaEventsTimeline, .displayKnockingRequests, .displayReportRoom: + case .requestNotificationSettingsPresentation, .requestRecipientDetailsPresentation, .requestInvitePeoplePresentation, .requestPollsHistoryPresentation, + .startCall, .displayPinnedEventsTimeline, .displayMediaEventsTimeline, .displayKnockingRequests, + .displayReportRoom, .transferOwnership: break // Not handled in this context case .requestEditDetailsPresentation: - break // TODO: + actionsSubject.send(.presentEditDetailsScreen) case .displaySecurityAndPrivacy: - break // TODO: - case .transferOwnership: - break // TODO: + actionsSubject.send(.presentSecurityAndPrivacyScreen) case .requestMemberDetailsPresentation: + actionsSubject.send(.presentMembersListScreen) + case .requestRolesAndPermissionsPresentation: + actionsSubject.send(.presentRolesAndPermissionsScreen) + case .leftRoom: break // TODO: } }