diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 1ed593712..24f25afe0 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -1346,7 +1346,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private func presentSecurityAndPrivacyScreen() { let coordinator = SecurityAndPrivacyScreenCoordinator(parameters: .init(roomProxy: roomProxy, clientProxy: userSession.clientProxy, - userIndicatorController: flowParameters.userIndicatorController)) + userIndicatorController: flowParameters.userIndicatorController, + appSetting: flowParameters.appSettings)) coordinator.actionsPublisher.sink { [weak self] action in guard let self else { return } diff --git a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift index 146f023d0..a48abd006 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift @@ -248,7 +248,8 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { private func presentSecurityAndPrivacyScreen() { let coordinator = SecurityAndPrivacyScreenCoordinator(parameters: .init(roomProxy: roomProxy, clientProxy: flowParameters.userSession.clientProxy, - userIndicatorController: flowParameters.userIndicatorController)) + userIndicatorController: flowParameters.userIndicatorController, + appSetting: flowParameters.appSettings)) coordinator.actionsPublisher.sink { [weak self] action in guard let self else { return } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index d9d77339c..13486bc17 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -57,6 +57,7 @@ struct RoomDetailsScreenViewState: BindableState { var canEditRoomTopic = false var canEditRoomAvatar = false var canEditRolesOrPermissions = false + var canEditSecurityAndPrivacy = false var canKickUsers = false var canBanUsers = false var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading @@ -74,7 +75,7 @@ struct RoomDetailsScreenViewState: BindableState { } var canSeeSecurityAndPrivacy: Bool { - knockingEnabled && dmRecipientInfo == nil && canEditRolesOrPermissions + dmRecipientInfo == nil && canEditSecurityAndPrivacy } var canEditBaseInfo: Bool { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index c65262278..b6eae7ea1 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -292,6 +292,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr state.canBanUsers = powerLevels.canOwnUserBan() state.canJoinCall = powerLevels.canOwnUserJoinCall() state.canEditRolesOrPermissions = powerLevels.canOwnUserEditRolesAndPermissions() + state.canEditSecurityAndPrivacy = powerLevels.canOwnUserEditSecurityAndPrivacy() } } diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift index 6aed70bf9..414f592d7 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift @@ -13,6 +13,7 @@ struct SecurityAndPrivacyScreenCoordinatorParameters { let roomProxy: JoinedRoomProxyProtocol let clientProxy: ClientProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol + let appSetting: AppSettings } enum SecurityAndPrivacyScreenCoordinatorAction { @@ -32,7 +33,8 @@ final class SecurityAndPrivacyScreenCoordinator: CoordinatorProtocol { init(parameters: SecurityAndPrivacyScreenCoordinatorParameters) { viewModel = SecurityAndPrivacyScreenViewModel(roomProxy: parameters.roomProxy, clientProxy: parameters.clientProxy, - userIndicatorController: parameters.userIndicatorController) + userIndicatorController: parameters.userIndicatorController, + appSettings: parameters.appSetting) } func start() { diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift index a3d803131..6de63c992 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift @@ -17,6 +17,13 @@ struct SecurityAndPrivacyScreenViewState: BindableState { var currentSettings: SecurityAndPrivacySettings var bindings: SecurityAndPrivacyScreenViewStateBindings var canonicalAlias: String? + var isKnockingEnabled: Bool + var isSpace: Bool + + var canEditAddress = false + var canEditJoinRule = false + var canEnableEncryption = false + var canEditHistoryVisibility = false private var hasChanges: Bool { currentSettings != bindings.desiredSettings @@ -42,8 +49,13 @@ struct SecurityAndPrivacyScreenViewState: BindableState { init(serverName: String, accessType: SecurityAndPrivacyRoomAccessType, isEncryptionEnabled: Bool, - historyVisibility: SecurityAndPrivacyHistoryVisibility) { + historyVisibility: SecurityAndPrivacyHistoryVisibility, + isSpace: Bool, + isKnockingEnabled: Bool) { self.serverName = serverName + self.isKnockingEnabled = isKnockingEnabled + self.isSpace = isSpace + let settings = SecurityAndPrivacySettings(accessType: accessType, isEncryptionEnabled: isEncryptionEnabled, historyVisibility: historyVisibility) diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift index 154709e16..c40fe4f0c 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift @@ -16,6 +16,7 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, private let roomProxy: JoinedRoomProxyProtocol private let clientProxy: ClientProxyProtocol private let userIndicatorController: UserIndicatorControllerProtocol + private let appSettings: AppSettings private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { @@ -24,14 +25,23 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, init(roomProxy: JoinedRoomProxyProtocol, clientProxy: ClientProxyProtocol, - userIndicatorController: UserIndicatorControllerProtocol) { + userIndicatorController: UserIndicatorControllerProtocol, + appSettings: AppSettings) { self.roomProxy = roomProxy self.clientProxy = clientProxy self.userIndicatorController = userIndicatorController + self.appSettings = appSettings + super.init(initialViewState: SecurityAndPrivacyScreenViewState(serverName: clientProxy.userIDServerName ?? "", accessType: roomProxy.infoPublisher.value.joinRule.toSecurityAndPrivacyRoomAccessType, isEncryptionEnabled: roomProxy.infoPublisher.value.isEncrypted, - historyVisibility: roomProxy.infoPublisher.value.historyVisibility.toSecurityAndPrivacyHistoryVisibility)) + historyVisibility: roomProxy.infoPublisher.value.historyVisibility.toSecurityAndPrivacyHistoryVisibility, + isSpace: roomProxy.infoPublisher.value.isSpace, + isKnockingEnabled: appSettings.knockingEnabled)) + + if let powerLevels = roomProxy.infoPublisher.value.powerLevels { + setupPermissions(powerLevels: powerLevels) + } setupRoomDirectoryVisibility() setupSubscriptions() @@ -82,7 +92,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, let userIDServerName = clientProxy.userIDServerName - roomProxy.infoPublisher + let infoPublisher = roomProxy.infoPublisher + infoPublisher .compactMap { roomInfo in guard let userIDServerName else { return nil @@ -96,6 +107,33 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, .receive(on: DispatchQueue.main) .weakAssign(to: \.state.canonicalAlias, on: self) .store(in: &cancellables) + + infoPublisher + .compactMap(\.powerLevels) + .removeDuplicates { $0.userPowerLevels == $1.userPowerLevels && $0.values == $1.values } + .receive(on: DispatchQueue.main) + .sink { [weak self] powerLevels in + self?.setupPermissions(powerLevels: powerLevels) + } + .store(in: &cancellables) + + infoPublisher + .map(\.isSpace) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.isSpace, on: self) + .store(in: &cancellables) + + appSettings.$knockingEnabled + .weakAssign(to: \.state.isKnockingEnabled, on: self) + .store(in: &cancellables) + } + + private func setupPermissions(powerLevels: RoomPowerLevelsProxyProtocol) { + state.canEditAddress = powerLevels.canOwnUser(sendStateEvent: .roomAliases) + state.canEditJoinRule = powerLevels.canOwnUser(sendStateEvent: .roomJoinRules) + state.canEditHistoryVisibility = powerLevels.canOwnUser(sendStateEvent: .roomHistoryVisibility) + state.canEnableEncryption = powerLevels.canOwnUser(sendStateEvent: .roomEncryption) } private func setupRoomDirectoryVisibility() { diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/View/SecurityAndPrivacyScreen.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/View/SecurityAndPrivacyScreen.swift index b522aca30..0406841e1 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/View/SecurityAndPrivacyScreen.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/View/SecurityAndPrivacyScreen.swift @@ -14,8 +14,11 @@ struct SecurityAndPrivacyScreen: View { var body: some View { Form { - roomAccessSection - if context.desiredSettings.accessType != .inviteOnly { + if context.viewState.canEditJoinRule { + roomAccessSection + } + + if context.desiredSettings.accessType != .inviteOnly, context.viewState.canEditAddress { if let canonicalAlias = context.viewState.canonicalAlias { visibilitySection addressSection(canonicalAlias: canonicalAlias) @@ -25,8 +28,15 @@ struct SecurityAndPrivacyScreen: View { addAddressSection } } - encryptionSection - historySection + + if !context.viewState.isSpace { + if context.viewState.canEnableEncryption { + encryptionSection + } + if context.viewState.canEditHistoryVisibility { + historySection + } + } } .compoundList() .navigationBarTitleDisplayMode(.inline) @@ -40,9 +50,12 @@ struct SecurityAndPrivacyScreen: View { ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle, description: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionDescription), kind: .selection(isSelected: context.desiredSettings.accessType == .inviteOnly) { context.desiredSettings.accessType = .inviteOnly }) - ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyAskToJoinOptionTitle, - description: L10n.screenSecurityAndPrivacyAskToJoinOptionDescription), - kind: .selection(isSelected: context.desiredSettings.accessType == .askToJoin) { context.desiredSettings.accessType = .askToJoin }) + if context.viewState.isKnockingEnabled || context.viewState.currentSettings.accessType == .askToJoin { + ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyAskToJoinOptionTitle, + description: L10n.screenSecurityAndPrivacyAskToJoinOptionDescription), + kind: .selection(isSelected: context.desiredSettings.accessType == .askToJoin) { context.desiredSettings.accessType = .askToJoin }) + .disabled(!context.viewState.isKnockingEnabled) + } ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomAccessAnyoneOptionTitle, description: L10n.screenSecurityAndPrivacyRoomAccessAnyoneOptionDescription), kind: .selection(isSelected: context.desiredSettings.accessType == .anyone) { context.desiredSettings.accessType = .anyone }) @@ -187,28 +200,76 @@ struct SecurityAndPrivacyScreen: View { // MARK: - Previews struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview { - static let inviteOnlyViewModel = SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(joinRule: .invite)), - clientProxy: ClientProxyMock(.init()), - userIndicatorController: UserIndicatorControllerMock()) + static let inviteOnlyViewModel = { + AppSettings.resetAllSettings() + return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsCreator, + joinRule: .invite)), + clientProxy: ClientProxyMock(.init()), + userIndicatorController: UserIndicatorControllerMock(), + appSettings: AppSettings()) + }() - static let publicViewModel = SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, - canonicalAlias: "#room:matrix.org", - joinRule: .public, - isVisibleInPublicDirectory: true)), - clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), - userIndicatorController: UserIndicatorControllerMock()) + static let publicViewModel = { + AppSettings.resetAllSettings() + return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, + canonicalAlias: "#room:matrix.org", + members: .allMembersAsCreator, + joinRule: .public, + isVisibleInPublicDirectory: true)), + clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), + userIndicatorController: UserIndicatorControllerMock(), + appSettings: AppSettings()) + }() - static let publicNoAddressViewModel = SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, - joinRule: .public)), - clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), - userIndicatorController: UserIndicatorControllerMock()) + static let publicNoAddressViewModel = { + AppSettings.resetAllSettings() + return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, + members: .allMembersAsCreator, + joinRule: .public)), + clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), + userIndicatorController: UserIndicatorControllerMock(), + appSettings: AppSettings()) + }() - static let restrictedViewModel = SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, - canonicalAlias: "#room:matrix.org", - joinRule: .restricted(rules: []), - isVisibleInPublicDirectory: true)), - clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), - userIndicatorController: UserIndicatorControllerMock()) + static let restrictedViewModel = { + AppSettings.resetAllSettings() + return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, + canonicalAlias: "#room:matrix.org", + members: .allMembersAsCreator, + joinRule: .restricted(rules: []), + isVisibleInPublicDirectory: true)), + clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), + userIndicatorController: UserIndicatorControllerMock(), + appSettings: AppSettings()) + }() + + static let askToJoinViewModel = { + AppSettings.resetAllSettings() + let appSettings = AppSettings() + appSettings.knockingEnabled = true + + return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, + canonicalAlias: "#room:matrix.org", + members: .allMembersAsCreator, + joinRule: .knock, + isVisibleInPublicDirectory: true)), + clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), + userIndicatorController: UserIndicatorControllerMock(), + appSettings: appSettings) + }() + + static let publicSpaceViewModel = { + AppSettings.resetAllSettings() + return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isSpace: true, + isEncrypted: false, + canonicalAlias: "#space:matrix.org", + members: .allMembersAsCreator, + joinRule: .public, + isVisibleInPublicDirectory: true)), + clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), + userIndicatorController: UserIndicatorControllerMock(), + appSettings: AppSettings()) + }() static var previews: some View { NavigationStack { @@ -236,5 +297,21 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview { state.currentSettings.isVisibileInRoomDirectory == true }) .previewDisplayName("Restricted room") + + NavigationStack { + SecurityAndPrivacyScreen(context: askToJoinViewModel.context) + } + .snapshotPreferences(expect: restrictedViewModel.context.$viewState.map { state in + state.currentSettings.isVisibileInRoomDirectory == true + }) + .previewDisplayName("Ask to join room") + + NavigationStack { + SecurityAndPrivacyScreen(context: publicSpaceViewModel.context) + } + .snapshotPreferences(expect: restrictedViewModel.context.$viewState.map { state in + state.currentSettings.isVisibileInRoomDirectory == true + }) + .previewDisplayName("Public space") } } diff --git a/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift index 98178f495..0c1cd64f6 100644 --- a/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift @@ -40,8 +40,13 @@ protocol RoomPowerLevelsProxyProtocol { // MARK: - Helpers extension RoomPowerLevelsProxyProtocol { - /// Can own user edit either the room name, avatar or topic + /// Can own user edit either the room name, avatar or topic. func canOwnUserEditBaseInfo() -> Bool { canOwnUser(sendStateEvent: .roomAvatar) || canOwnUser(sendStateEvent: .roomName) || canOwnUser(sendStateEvent: .roomTopic) } + + /// Can own user edit any of the security and privacy settings. + func canOwnUserEditSecurityAndPrivacy() -> Bool { + canOwnUser(sendStateEvent: .roomEncryption) || canOwnUser(sendStateEvent: .roomAliases) || canOwnUser(sendStateEvent: .roomJoinRules) || canOwnUser(sendStateEvent: .roomHistoryVisibility) + } }