Always show security and privacy, and just hide the knocking jojn rule option through the feature flag

This commit is contained in:
Mauro Romito
2025-11-10 18:56:20 +01:00
committed by Mauro
parent dd95a4313a
commit beb7900017
9 changed files with 173 additions and 35 deletions
@@ -1346,7 +1346,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private func presentSecurityAndPrivacyScreen() { private func presentSecurityAndPrivacyScreen() {
let coordinator = SecurityAndPrivacyScreenCoordinator(parameters: .init(roomProxy: roomProxy, let coordinator = SecurityAndPrivacyScreenCoordinator(parameters: .init(roomProxy: roomProxy,
clientProxy: userSession.clientProxy, clientProxy: userSession.clientProxy,
userIndicatorController: flowParameters.userIndicatorController)) userIndicatorController: flowParameters.userIndicatorController,
appSetting: flowParameters.appSettings))
coordinator.actionsPublisher.sink { [weak self] action in coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return } guard let self else { return }
@@ -248,7 +248,8 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
private func presentSecurityAndPrivacyScreen() { private func presentSecurityAndPrivacyScreen() {
let coordinator = SecurityAndPrivacyScreenCoordinator(parameters: .init(roomProxy: roomProxy, let coordinator = SecurityAndPrivacyScreenCoordinator(parameters: .init(roomProxy: roomProxy,
clientProxy: flowParameters.userSession.clientProxy, clientProxy: flowParameters.userSession.clientProxy,
userIndicatorController: flowParameters.userIndicatorController)) userIndicatorController: flowParameters.userIndicatorController,
appSetting: flowParameters.appSettings))
coordinator.actionsPublisher.sink { [weak self] action in coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return } guard let self else { return }
@@ -57,6 +57,7 @@ struct RoomDetailsScreenViewState: BindableState {
var canEditRoomTopic = false var canEditRoomTopic = false
var canEditRoomAvatar = false var canEditRoomAvatar = false
var canEditRolesOrPermissions = false var canEditRolesOrPermissions = false
var canEditSecurityAndPrivacy = false
var canKickUsers = false var canKickUsers = false
var canBanUsers = false var canBanUsers = false
var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading
@@ -74,7 +75,7 @@ struct RoomDetailsScreenViewState: BindableState {
} }
var canSeeSecurityAndPrivacy: Bool { var canSeeSecurityAndPrivacy: Bool {
knockingEnabled && dmRecipientInfo == nil && canEditRolesOrPermissions dmRecipientInfo == nil && canEditSecurityAndPrivacy
} }
var canEditBaseInfo: Bool { var canEditBaseInfo: Bool {
@@ -292,6 +292,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.canBanUsers = powerLevels.canOwnUserBan() state.canBanUsers = powerLevels.canOwnUserBan()
state.canJoinCall = powerLevels.canOwnUserJoinCall() state.canJoinCall = powerLevels.canOwnUserJoinCall()
state.canEditRolesOrPermissions = powerLevels.canOwnUserEditRolesAndPermissions() state.canEditRolesOrPermissions = powerLevels.canOwnUserEditRolesAndPermissions()
state.canEditSecurityAndPrivacy = powerLevels.canOwnUserEditSecurityAndPrivacy()
} }
} }
@@ -13,6 +13,7 @@ struct SecurityAndPrivacyScreenCoordinatorParameters {
let roomProxy: JoinedRoomProxyProtocol let roomProxy: JoinedRoomProxyProtocol
let clientProxy: ClientProxyProtocol let clientProxy: ClientProxyProtocol
let userIndicatorController: UserIndicatorControllerProtocol let userIndicatorController: UserIndicatorControllerProtocol
let appSetting: AppSettings
} }
enum SecurityAndPrivacyScreenCoordinatorAction { enum SecurityAndPrivacyScreenCoordinatorAction {
@@ -32,7 +33,8 @@ final class SecurityAndPrivacyScreenCoordinator: CoordinatorProtocol {
init(parameters: SecurityAndPrivacyScreenCoordinatorParameters) { init(parameters: SecurityAndPrivacyScreenCoordinatorParameters) {
viewModel = SecurityAndPrivacyScreenViewModel(roomProxy: parameters.roomProxy, viewModel = SecurityAndPrivacyScreenViewModel(roomProxy: parameters.roomProxy,
clientProxy: parameters.clientProxy, clientProxy: parameters.clientProxy,
userIndicatorController: parameters.userIndicatorController) userIndicatorController: parameters.userIndicatorController,
appSettings: parameters.appSetting)
} }
func start() { func start() {
@@ -17,6 +17,13 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
var currentSettings: SecurityAndPrivacySettings var currentSettings: SecurityAndPrivacySettings
var bindings: SecurityAndPrivacyScreenViewStateBindings var bindings: SecurityAndPrivacyScreenViewStateBindings
var canonicalAlias: String? 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 { private var hasChanges: Bool {
currentSettings != bindings.desiredSettings currentSettings != bindings.desiredSettings
@@ -42,8 +49,13 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
init(serverName: String, init(serverName: String,
accessType: SecurityAndPrivacyRoomAccessType, accessType: SecurityAndPrivacyRoomAccessType,
isEncryptionEnabled: Bool, isEncryptionEnabled: Bool,
historyVisibility: SecurityAndPrivacyHistoryVisibility) { historyVisibility: SecurityAndPrivacyHistoryVisibility,
isSpace: Bool,
isKnockingEnabled: Bool) {
self.serverName = serverName self.serverName = serverName
self.isKnockingEnabled = isKnockingEnabled
self.isSpace = isSpace
let settings = SecurityAndPrivacySettings(accessType: accessType, let settings = SecurityAndPrivacySettings(accessType: accessType,
isEncryptionEnabled: isEncryptionEnabled, isEncryptionEnabled: isEncryptionEnabled,
historyVisibility: historyVisibility) historyVisibility: historyVisibility)
@@ -16,6 +16,7 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
private let roomProxy: JoinedRoomProxyProtocol private let roomProxy: JoinedRoomProxyProtocol
private let clientProxy: ClientProxyProtocol private let clientProxy: ClientProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
private let appSettings: AppSettings
private let actionsSubject: PassthroughSubject<SecurityAndPrivacyScreenViewModelAction, Never> = .init() private let actionsSubject: PassthroughSubject<SecurityAndPrivacyScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<SecurityAndPrivacyScreenViewModelAction, Never> { var actionsPublisher: AnyPublisher<SecurityAndPrivacyScreenViewModelAction, Never> {
@@ -24,14 +25,23 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
init(roomProxy: JoinedRoomProxyProtocol, init(roomProxy: JoinedRoomProxyProtocol,
clientProxy: ClientProxyProtocol, clientProxy: ClientProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol) { userIndicatorController: UserIndicatorControllerProtocol,
appSettings: AppSettings) {
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.clientProxy = clientProxy self.clientProxy = clientProxy
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.appSettings = appSettings
super.init(initialViewState: SecurityAndPrivacyScreenViewState(serverName: clientProxy.userIDServerName ?? "", super.init(initialViewState: SecurityAndPrivacyScreenViewState(serverName: clientProxy.userIDServerName ?? "",
accessType: roomProxy.infoPublisher.value.joinRule.toSecurityAndPrivacyRoomAccessType, accessType: roomProxy.infoPublisher.value.joinRule.toSecurityAndPrivacyRoomAccessType,
isEncryptionEnabled: roomProxy.infoPublisher.value.isEncrypted, 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() setupRoomDirectoryVisibility()
setupSubscriptions() setupSubscriptions()
@@ -82,7 +92,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
let userIDServerName = clientProxy.userIDServerName let userIDServerName = clientProxy.userIDServerName
roomProxy.infoPublisher let infoPublisher = roomProxy.infoPublisher
infoPublisher
.compactMap { roomInfo in .compactMap { roomInfo in
guard let userIDServerName else { guard let userIDServerName else {
return nil return nil
@@ -96,6 +107,33 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.weakAssign(to: \.state.canonicalAlias, on: self) .weakAssign(to: \.state.canonicalAlias, on: self)
.store(in: &cancellables) .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() { private func setupRoomDirectoryVisibility() {
@@ -14,8 +14,11 @@ struct SecurityAndPrivacyScreen: View {
var body: some View { var body: some View {
Form { Form {
roomAccessSection if context.viewState.canEditJoinRule {
if context.desiredSettings.accessType != .inviteOnly { roomAccessSection
}
if context.desiredSettings.accessType != .inviteOnly, context.viewState.canEditAddress {
if let canonicalAlias = context.viewState.canonicalAlias { if let canonicalAlias = context.viewState.canonicalAlias {
visibilitySection visibilitySection
addressSection(canonicalAlias: canonicalAlias) addressSection(canonicalAlias: canonicalAlias)
@@ -25,8 +28,15 @@ struct SecurityAndPrivacyScreen: View {
addAddressSection addAddressSection
} }
} }
encryptionSection
historySection if !context.viewState.isSpace {
if context.viewState.canEnableEncryption {
encryptionSection
}
if context.viewState.canEditHistoryVisibility {
historySection
}
}
} }
.compoundList() .compoundList()
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
@@ -40,9 +50,12 @@ struct SecurityAndPrivacyScreen: View {
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle, ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle,
description: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionDescription), description: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionDescription),
kind: .selection(isSelected: context.desiredSettings.accessType == .inviteOnly) { context.desiredSettings.accessType = .inviteOnly }) kind: .selection(isSelected: context.desiredSettings.accessType == .inviteOnly) { context.desiredSettings.accessType = .inviteOnly })
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyAskToJoinOptionTitle, if context.viewState.isKnockingEnabled || context.viewState.currentSettings.accessType == .askToJoin {
description: L10n.screenSecurityAndPrivacyAskToJoinOptionDescription), ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyAskToJoinOptionTitle,
kind: .selection(isSelected: context.desiredSettings.accessType == .askToJoin) { context.desiredSettings.accessType = .askToJoin }) description: L10n.screenSecurityAndPrivacyAskToJoinOptionDescription),
kind: .selection(isSelected: context.desiredSettings.accessType == .askToJoin) { context.desiredSettings.accessType = .askToJoin })
.disabled(!context.viewState.isKnockingEnabled)
}
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomAccessAnyoneOptionTitle, ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomAccessAnyoneOptionTitle,
description: L10n.screenSecurityAndPrivacyRoomAccessAnyoneOptionDescription), description: L10n.screenSecurityAndPrivacyRoomAccessAnyoneOptionDescription),
kind: .selection(isSelected: context.desiredSettings.accessType == .anyone) { context.desiredSettings.accessType = .anyone }) kind: .selection(isSelected: context.desiredSettings.accessType == .anyone) { context.desiredSettings.accessType = .anyone })
@@ -187,28 +200,76 @@ struct SecurityAndPrivacyScreen: View {
// MARK: - Previews // MARK: - Previews
struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview { struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
static let inviteOnlyViewModel = SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(joinRule: .invite)), static let inviteOnlyViewModel = {
clientProxy: ClientProxyMock(.init()), AppSettings.resetAllSettings()
userIndicatorController: UserIndicatorControllerMock()) 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, static let publicViewModel = {
canonicalAlias: "#room:matrix.org", AppSettings.resetAllSettings()
joinRule: .public, return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
isVisibleInPublicDirectory: true)), canonicalAlias: "#room:matrix.org",
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), members: .allMembersAsCreator,
userIndicatorController: UserIndicatorControllerMock()) joinRule: .public,
isVisibleInPublicDirectory: true)),
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")),
userIndicatorController: UserIndicatorControllerMock(),
appSettings: AppSettings())
}()
static let publicNoAddressViewModel = SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, static let publicNoAddressViewModel = {
joinRule: .public)), AppSettings.resetAllSettings()
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
userIndicatorController: UserIndicatorControllerMock()) members: .allMembersAsCreator,
joinRule: .public)),
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")),
userIndicatorController: UserIndicatorControllerMock(),
appSettings: AppSettings())
}()
static let restrictedViewModel = SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, static let restrictedViewModel = {
canonicalAlias: "#room:matrix.org", AppSettings.resetAllSettings()
joinRule: .restricted(rules: []), return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
isVisibleInPublicDirectory: true)), canonicalAlias: "#room:matrix.org",
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")), members: .allMembersAsCreator,
userIndicatorController: UserIndicatorControllerMock()) 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 { static var previews: some View {
NavigationStack { NavigationStack {
@@ -236,5 +297,21 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
state.currentSettings.isVisibileInRoomDirectory == true state.currentSettings.isVisibileInRoomDirectory == true
}) })
.previewDisplayName("Restricted room") .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")
} }
} }
@@ -40,8 +40,13 @@ protocol RoomPowerLevelsProxyProtocol {
// MARK: - Helpers // MARK: - Helpers
extension RoomPowerLevelsProxyProtocol { 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 { func canOwnUserEditBaseInfo() -> Bool {
canOwnUser(sendStateEvent: .roomAvatar) || canOwnUser(sendStateEvent: .roomName) || canOwnUser(sendStateEvent: .roomTopic) 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)
}
} }