implemented a way to filter out roles that can't be assigned to permission, and disable permissions that are above the current power level of the user.

This commit is contained in:
Mauro Romito
2025-12-18 18:43:19 +01:00
committed by Mauro
parent f2fc164e2b
commit 88cd816392
18 changed files with 197 additions and 90 deletions
@@ -119,10 +119,10 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.addRoutes(event: .finishedChangingRoles, transitions: [.changingRoles => .rolesAndPermissionsScreen])
stateMachine.addRoutes(event: .changePermissions, transitions: [.rolesAndPermissionsScreen => .changingPermissions]) { [weak self] context in
guard let permissions = context.userInfo as? RoomPermissions else {
guard let (ownPowerLevel, permissions) = context.userInfo as? (RoomPowerLevel, RoomPermissions) else {
fatalError("Expected a group and the current permissions")
}
self?.presentChangePermissionsScreen(permissions: permissions)
self?.presentChangePermissionsScreen(ownPowerLevel: ownPowerLevel, permissions: permissions)
}
stateMachine.addRoutes(event: .finishedChangingPermissions, transitions: [.changingPermissions => .rolesAndPermissionsScreen])
@@ -144,8 +144,8 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .editRoles(let role):
stateMachine.tryEvent(.changeRoles, userInfo: role)
case .editPermissions(let permissions):
stateMachine.tryEvent(.changePermissions, userInfo: permissions)
case .editPermissions(let ownPowerLevel, let permissions):
stateMachine.tryEvent(.changePermissions, userInfo: (ownPowerLevel, permissions))
case .demotedOwnUser:
stateMachine.tryEvent(.demotedOwnUser)
}
@@ -179,8 +179,9 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentChangePermissionsScreen(permissions: RoomPermissions) {
let parameters = RoomChangePermissionsScreenCoordinatorParameters(permissions: permissions,
private func presentChangePermissionsScreen(ownPowerLevel: RoomPowerLevel, permissions: RoomPermissions) {
let parameters = RoomChangePermissionsScreenCoordinatorParameters(ownPowerLevel: ownPowerLevel,
permissions: permissions,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
analytics: analytics)
@@ -10,6 +10,7 @@ import Combine
import SwiftUI
struct RoomChangePermissionsScreenCoordinatorParameters {
let ownPowerLevel: RoomPowerLevel
let permissions: RoomPermissions
let roomProxy: JoinedRoomProxyProtocol
let userIndicatorController: UserIndicatorControllerProtocol
@@ -21,7 +22,6 @@ enum RoomChangePermissionsScreenCoordinatorAction {
}
final class RoomChangePermissionsScreenCoordinator: CoordinatorProtocol {
private let parameters: RoomChangePermissionsScreenCoordinatorParameters
private var viewModel: RoomChangePermissionsScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
@@ -31,9 +31,8 @@ final class RoomChangePermissionsScreenCoordinator: CoordinatorProtocol {
}
init(parameters: RoomChangePermissionsScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = RoomChangePermissionsScreenViewModel(currentPermissions: parameters.permissions,
ownPowerLevel: parameters.ownPowerLevel,
roomProxy: parameters.roomProxy,
userIndicatorController: parameters.userIndicatorController,
analytics: parameters.analytics)
@@ -69,7 +69,7 @@ extension RoomChangePermissionsScreenViewState {
/// - Parameters:
/// - currentPermissions: The current permissions for the room.
/// - isSpace: if the room is a space or a normal room.
init(currentPermissions: RoomPermissions, isSpace: Bool) {
init(ownPowerLevel: RoomPowerLevel, currentPermissions: RoomPermissions, isSpace: Bool) {
var settings = [RoomChangePermissionsScreenGroup: [RoomPermissionsSetting]]()
for group in RoomChangePermissionsScreenGroup.allCases {
switch group {
@@ -77,12 +77,15 @@ extension RoomChangePermissionsScreenViewState {
settings[group] = [
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsRoomName,
value: currentPermissions.roomName,
ownPowerLevel: ownPowerLevel,
keyPath: \.roomName),
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsRoomAvatar,
value: currentPermissions.roomAvatar,
ownPowerLevel: ownPowerLevel,
keyPath: \.roomAvatar),
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsRoomTopic,
value: currentPermissions.roomTopic,
ownPowerLevel: ownPowerLevel,
keyPath: \.roomTopic)
]
@@ -90,12 +93,15 @@ extension RoomChangePermissionsScreenViewState {
settings[group] = [
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsInvitePeople,
value: currentPermissions.invite,
ownPowerLevel: ownPowerLevel,
keyPath: \.invite),
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsRemovePeople,
value: currentPermissions.kick,
ownPowerLevel: ownPowerLevel,
keyPath: \.kick),
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsBanPeople,
value: currentPermissions.ban,
ownPowerLevel: ownPowerLevel,
keyPath: \.ban)
]
case .messagesAndContent:
@@ -103,9 +109,11 @@ extension RoomChangePermissionsScreenViewState {
settings[group] = [
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsSendMessages,
value: currentPermissions.eventsDefault,
ownPowerLevel: ownPowerLevel,
keyPath: \.eventsDefault),
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsDeleteMessages,
value: currentPermissions.redact,
ownPowerLevel: ownPowerLevel,
keyPath: \.redact)
]
}
@@ -114,6 +122,7 @@ extension RoomChangePermissionsScreenViewState {
settings[group] = [
RoomPermissionsSetting(title: L10n.screenRoomChangePermissionsManageSpaceRooms,
value: currentPermissions.spaceChild,
ownPowerLevel: ownPowerLevel,
keyPath: \.spaceChild)
]
}
@@ -23,13 +23,16 @@ class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModel
}
init(currentPermissions: RoomPermissions,
ownPowerLevel: RoomPowerLevel,
roomProxy: JoinedRoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
analytics: AnalyticsService) {
self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController
self.analytics = analytics
super.init(initialViewState: .init(currentPermissions: currentPermissions, isSpace: roomProxy.infoPublisher.value.isSpace))
super.init(initialViewState: .init(ownPowerLevel: ownPowerLevel,
currentPermissions: currentPermissions,
isSpace: roomProxy.infoPublisher.value.isSpace))
}
// MARK: - Public
@@ -64,7 +67,7 @@ class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModel
.flatMap { $0 }
.filter { state.currentPermissions[keyPath: $0.keyPath] != $0.value }
for setting in changedSettings {
changes[keyPath: setting.rustKeyPath] = setting.value.powerLevelValue
changes[keyPath: setting.rustKeyPath] = setting.value
}
switch await roomProxy.applyPowerLevelChanges(changes) {
@@ -106,14 +109,14 @@ class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModel
private func trackChanges(_ settings: [RoomPermissionsSetting]) {
for setting in settings {
switch setting.keyPath {
case \.ban: analytics.trackRoomModeration(action: .ChangePermissionsBanMembers, role: setting.value)
case \.invite: analytics.trackRoomModeration(action: .ChangePermissionsInviteUsers, role: setting.value)
case \.kick: analytics.trackRoomModeration(action: .ChangePermissionsKickMembers, role: setting.value)
case \.redact: analytics.trackRoomModeration(action: .ChangePermissionsRedactMessages, role: setting.value)
case \.eventsDefault: analytics.trackRoomModeration(action: .ChangePermissionsSendMessages, role: setting.value)
case \.roomName: analytics.trackRoomModeration(action: .ChangePermissionsRoomName, role: setting.value)
case \.roomAvatar: analytics.trackRoomModeration(action: .ChangePermissionsRoomAvatar, role: setting.value)
case \.roomTopic: analytics.trackRoomModeration(action: .ChangePermissionsRoomTopic, role: setting.value)
case \.ban: analytics.trackRoomModeration(action: .ChangePermissionsBanMembers, role: setting.roleValue)
case \.invite: analytics.trackRoomModeration(action: .ChangePermissionsInviteUsers, role: setting.roleValue)
case \.kick: analytics.trackRoomModeration(action: .ChangePermissionsKickMembers, role: setting.roleValue)
case \.redact: analytics.trackRoomModeration(action: .ChangePermissionsRedactMessages, role: setting.roleValue)
case \.eventsDefault: analytics.trackRoomModeration(action: .ChangePermissionsSendMessages, role: setting.roleValue)
case \.roomName: analytics.trackRoomModeration(action: .ChangePermissionsRoomName, role: setting.roleValue)
case \.roomAvatar: analytics.trackRoomModeration(action: .ChangePermissionsRoomAvatar, role: setting.roleValue)
case \.roomTopic: analytics.trackRoomModeration(action: .ChangePermissionsRoomTopic, role: setting.roleValue)
default: MXLog.warning("Unexpected change: \(setting.keyPath).")
}
}
@@ -32,7 +32,9 @@ struct RoomChangePermissionsScreen: View {
Section {
ForEach(settings) { $setting in
ListRow(label: .plain(title: setting.title),
kind: .picker(selection: $setting.value, items: setting.allValues))
kind: .picker(selection: $setting.roleValue,
items: setting.availableValues))
.disabled(setting.isDisabled)
}
} header: {
Text(group.name)
@@ -64,6 +66,7 @@ struct RoomChangePermissionsScreen: View {
struct RoomChangePermissionsScreen_Previews: PreviewProvider, TestablePreview {
static let roomViewModel = makeViewModel(isSpace: false)
static let roomAsUserViewModel = makeViewModel(isSpace: false, ownPowerLevel: RoomRole.user.powerLevel)
static let spaceViewModel = makeViewModel(isSpace: true)
static var previews: some View {
@@ -72,14 +75,20 @@ struct RoomChangePermissionsScreen_Previews: PreviewProvider, TestablePreview {
}
.previewDisplayName("Room")
NavigationStack {
RoomChangePermissionsScreen(context: roomAsUserViewModel.context)
}
.previewDisplayName("Room as User")
NavigationStack {
RoomChangePermissionsScreen(context: spaceViewModel.context)
}
.previewDisplayName("Space")
}
static func makeViewModel(isSpace: Bool) -> RoomChangePermissionsScreenViewModel {
static func makeViewModel(isSpace: Bool, ownPowerLevel: RoomPowerLevel = RoomRole.creator.powerLevel) -> RoomChangePermissionsScreenViewModel {
RoomChangePermissionsScreenViewModel(currentPermissions: .init(powerLevels: .mock),
ownPowerLevel: ownPowerLevel,
roomProxy: JoinedRoomProxyMock(.init(isSpace: isSpace)),
userIndicatorController: UserIndicatorControllerMock(),
analytics: ServiceLocator.shared.analytics)
@@ -17,7 +17,7 @@ struct RoomRolesAndPermissionsScreenCoordinatorParameters {
enum RoomRolesAndPermissionsScreenCoordinatorAction {
case editRoles(RoomRolesAndPermissionsScreenRole)
case editPermissions(permissions: RoomPermissions)
case editPermissions(ownPowerLevel: RoomPowerLevel, permissions: RoomPermissions)
case demotedOwnUser
}
@@ -44,8 +44,8 @@ final class RoomRolesAndPermissionsScreenCoordinator: CoordinatorProtocol {
switch action {
case .editRoles(let role):
actionsSubject.send(.editRoles(role))
case .editPermissions(let permissions):
actionsSubject.send(.editPermissions(permissions: permissions))
case .editPermissions(let ownPowerLevel, let permissions):
actionsSubject.send(.editPermissions(ownPowerLevel: ownPowerLevel, permissions: permissions))
case .demotedOwnUser:
actionsSubject.send(.demotedOwnUser)
}
@@ -12,13 +12,13 @@ enum RoomRolesAndPermissionsScreenViewModelAction {
/// The user would like to edit member roles.
case editRoles(RoomRolesAndPermissionsScreenRole)
/// The user would like to edit room permissions.
case editPermissions(permissions: RoomPermissions)
case editPermissions(ownPowerLevel: RoomPowerLevel, permissions: RoomPermissions)
/// The user has demoted themself.
case demotedOwnUser
}
struct RoomRolesAndPermissionsScreenViewState: BindableState {
var ownRole: RoomRole
var ownPowerLevel: RoomPowerLevel
var administratorsAndOwnersCount: Int?
/// The number of administrators in the room.
@@ -26,7 +26,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController
self.analytics = analytics
super.init(initialViewState: RoomRolesAndPermissionsScreenViewState(ownRole: roomProxy.membersPublisher.value.first { $0.userID == roomProxy.ownUserID }?.role ?? .administrator,
super.init(initialViewState: RoomRolesAndPermissionsScreenViewState(ownPowerLevel: roomProxy.membersPublisher.value.first { $0.userID == roomProxy.ownUserID }?.powerLevel ?? .value(Int(RoomRole.administrator.powerLevelValue)),
permissions: initialPermissions))
// Automatically update the admin/moderator counts.
@@ -91,7 +91,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
state.administratorCount = members.filter { $0.role == .administrator && $0.isActive }.count
state.moderatorCount = members.filter { $0.role == .moderator && $0.isActive }.count
if let ownUser = members.first(where: { $0.userID == roomProxy.ownUserID }) {
state.ownRole = ownUser.role
state.ownPowerLevel = ownUser.powerLevel
}
}
@@ -132,7 +132,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
MXLog.error("Missing permissions.")
return
}
actionsSubject.send(.editPermissions(permissions: permissions))
actionsSubject.send(.editPermissions(ownPowerLevel: state.ownPowerLevel, permissions: permissions))
}
private func resetPermissions() async {
@@ -27,7 +27,7 @@ struct RoomRolesAndPermissionsScreen: View {
private var rolesSection: some View {
Section {
if context.viewState.ownRole == .creator {
if context.viewState.ownPowerLevel.role == .creator {
ListRow(label: .default(title: L10n.screenRoomRolesAndPermissionsAdminsAndOwners,
icon: \.admin),
details: administratorOrOwnersDetails,
@@ -53,7 +53,7 @@ struct RoomRolesAndPermissionsScreen: View {
})
.accessibilityIdentifier(A11yIdentifiers.roomRolesAndPermissionsScreen.moderators)
if context.viewState.ownRole != .creator {
if context.viewState.ownPowerLevel.role != .creator {
ListRow(label: .default(title: L10n.screenRoomRolesAndPermissionsChangeMyRole,
icon: \.edit),
kind: .button {
@@ -10,24 +10,29 @@ import Foundation
import MatrixRustSDK
struct RoomPermissionsSetting: Identifiable {
var id: KeyPath<RoomPermissions, RoomRole> { keyPath }
static let allValues: [(title: String, tag: RoomRole)] = [(title: L10n.screenRoomChangePermissionsAdministrators, tag: .administrator),
(title: L10n.screenRoomChangePermissionsModerators, tag: .moderator),
(title: L10n.screenRoomChangePermissionsEveryone, tag: .user)]
var id: KeyPath<RoomPermissions, Int64> { keyPath }
/// The title of this setting.
let title: String
/// The selected role of this setting.
var value: RoomRole
/// All of the available roles that this setting can be configured with.
var allValues: [(title: String, tag: RoomRole)] {
[
(title: L10n.screenRoomChangePermissionsAdministrators, tag: .administrator),
(title: L10n.screenRoomChangePermissionsModerators, tag: .moderator),
(title: L10n.screenRoomChangePermissionsEveryone, tag: .user)
]
}
var value: Int64
let ownPowerLevel: RoomPowerLevel
var roleValue: RoomRole {
get {
RoomRole(powerLevelValue: value)
} set {
value = newValue.powerLevelValue
}
}
/// The `RoomPermissions` property that this setting is for.
let keyPath: KeyPath<RoomPermissions, RoomRole>
let keyPath: KeyPath<RoomPermissions, Int64>
/// The `RoomPowerLevelChanges` property that this setting is saved into.
var rustKeyPath: WritableKeyPath<RoomPowerLevelChanges, Int64?> {
switch keyPath {
@@ -45,46 +50,75 @@ struct RoomPermissionsSetting: Identifiable {
default: fatalError("Unexpected key path: \(keyPath)")
}
}
/// Can the setting be edited
var isDisabled: Bool {
switch ownPowerLevel {
case .value(let ownValue):
ownValue < value
case .infinite:
false
}
}
/// All of the available roles that this setting can be configured with.
var availableValues: [(title: String, tag: RoomRole)] {
if isDisabled {
Self.allValues.filter { $0.tag == RoomRole(powerLevelValue: value) }
} else {
Self.allValues.filter { $0.tag <= ownPowerLevel.role }
}
}
init(title: String,
value: Int64,
ownPowerLevel: RoomPowerLevel,
keyPath: KeyPath<RoomPermissions, Int64>) {
self.ownPowerLevel = ownPowerLevel
self.title = title
self.value = value
self.keyPath = keyPath
}
}
struct RoomPermissions {
/// The level required to ban a user.
var ban: RoomRole
var ban: Int64
/// The level required to invite a user.
var invite: RoomRole
var invite: Int64
/// The level required to kick a user.
var kick: RoomRole
var kick: Int64
/// The level required to redact an event.
var redact: RoomRole
var redact: Int64
/// The default level required to send message events.
var eventsDefault: RoomRole
var eventsDefault: Int64
/// The default level required to send state events.
var stateDefault: RoomRole
var stateDefault: Int64
/// The default power level for every user in the room.
var usersDefault: RoomRole
var usersDefault: Int64
/// The level required to change the room's name.
var roomName: RoomRole
var roomName: Int64
/// The level required to change the room's avatar.
var roomAvatar: RoomRole
var roomAvatar: Int64
/// The level required to change the room's topic.
var roomTopic: RoomRole
var roomTopic: Int64
/// The level required to add/remove childrens from a space.
var spaceChild: RoomRole
var spaceChild: Int64
}
extension RoomPermissions {
/// Create permissions from the room's power levels.
init(powerLevels: RoomPowerLevelsValues) {
ban = RoomRole(powerLevelValue: powerLevels.ban)
invite = RoomRole(powerLevelValue: powerLevels.invite)
kick = RoomRole(powerLevelValue: powerLevels.kick)
redact = RoomRole(powerLevelValue: powerLevels.redact)
eventsDefault = RoomRole(powerLevelValue: powerLevels.eventsDefault)
stateDefault = RoomRole(powerLevelValue: powerLevels.stateDefault)
usersDefault = RoomRole(powerLevelValue: powerLevels.usersDefault)
roomName = RoomRole(powerLevelValue: powerLevels.roomName)
roomAvatar = RoomRole(powerLevelValue: powerLevels.roomAvatar)
roomTopic = RoomRole(powerLevelValue: powerLevels.roomTopic)
spaceChild = RoomRole(powerLevelValue: powerLevels.spaceChild)
ban = powerLevels.ban
invite = powerLevels.invite
kick = powerLevels.kick
redact = powerLevels.redact
eventsDefault = powerLevels.eventsDefault
stateDefault = powerLevels.stateDefault
usersDefault = powerLevels.usersDefault
roomName = powerLevels.roomName
roomAvatar = powerLevels.roomAvatar
roomTopic = powerLevels.roomTopic
spaceChild = powerLevels.spaceChild
}
}
@@ -30,6 +30,10 @@ enum RoomPowerLevel: Hashable, Comparable {
case .value(let value): .value(value: Int64(value))
}
}
var role: RoomRole {
RoomRole(powerLevel: self)
}
}
extension PowerLevel {
+12 -8
View File
@@ -110,21 +110,25 @@ extension RoomRole {
/// To be used when setting the power level of a user to get the suggested equivalent power level value for that specific role
/// NOTE: Do not use for comparison, use the true power level instead.
var powerLevelValue: Int64 {
switch powerLevel {
case .infinite:
fatalError("Impossible")
case .value(let value):
return Int64(value)
}
}
var powerLevel: RoomPowerLevel {
guard self != .owner else {
// Would be better if the SDK would return this, maybe a `suggestedPowerLevelValueForRole` function would solve the problem
return 150
return .value(150)
}
do {
switch try suggestedPowerLevelForRole(role: rustRole) {
case .infinite:
fatalError("Impossible")
case .value(let value):
return value
}
return try RoomPowerLevel(rustPowerLevel: suggestedPowerLevelForRole(role: rustRole))
} catch {
MXLog.error("Falied to convert role to power level value: \(error)")
return 0
return .value(0)
}
}
}