Space Settings - Navigations (#4691)

* Implementation for all navigations inside the space settings aside the left space action

* pr suggestions
This commit is contained in:
Mauro
2025-11-05 18:07:44 +01:00
committed by GitHub
parent 095e040b2e
commit cddb4d4053
10 changed files with 287 additions and 17 deletions

View File

@@ -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)

View File

@@ -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<State, Event>
private var cancellables = Set<AnyCancellable>()
private var membersFlowCoordinator: RoomMembersFlowCoordinator?
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
private let actionsSubject: PassthroughSubject<SpaceSettingsFlowCoordinatorAction, Never> = .init()
var actions: AnyPublisher<SpaceSettingsFlowCoordinatorAction, Never> {
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)
}
}

View File

@@ -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<CGFloat>) -> 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<CGFloat>) {
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())
}
}

View File

@@ -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)
}

View File

@@ -23,6 +23,7 @@ struct RoomDetailsEditScreenViewStateBindings {
struct RoomDetailsEditScreenViewState: BindableState {
let roomID: String
let isSpace: Bool
let initialAvatarURL: URL?
let initialName: String
let initialTopic: String

View File

@@ -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 ?? "",

View File

@@ -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)

View File

@@ -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)
})
}
}

View File

@@ -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) {

View File

@@ -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:
}
}