From f13d9259c5baff59515769967deb6d55918f3405 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 19 Dec 2025 17:10:28 +0100 Subject: [PATCH] change(room permissions): user can edit only roles <= to his own role --- .../ChangeRoomPermissionsPresenter.kt | 8 ++++ .../permissions/ChangeRoomPermissionsState.kt | 47 ++++++++++++++----- .../ChangeRoomPermissionsStateProvider.kt | 3 ++ .../permissions/ChangeRoomPermissionsView.kt | 3 +- .../ChangeRoomPermissionsPresenterTest.kt | 38 ++++++++++++++- 5 files changed, 84 insertions(+), 15 deletions(-) diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenter.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenter.kt index 0032af6b01..552f1d0ec6 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenter.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenter.kt @@ -10,6 +10,7 @@ package io.element.android.features.rolesandpermissions.impl.permissions import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -20,9 +21,11 @@ import dev.zacsweers.metro.Inject import io.element.android.features.rolesandpermissions.impl.analytics.trackPermissionChangeAnalytics import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.ui.model.powerLevelOf import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableMap @@ -89,6 +92,10 @@ class ChangeRoomPermissionsPresenter( derivedStateOf { initialPermissions != currentPermissions } } + val ownPowerLevel by remember { + room.roomInfoFlow.mapState { it.powerLevelOf(room.sessionId) } + }.collectAsState() + fun handleEvent(event: ChangeRoomPermissionsEvent) { when (event) { is ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction -> { @@ -123,6 +130,7 @@ class ChangeRoomPermissionsPresenter( } } return ChangeRoomPermissionsState( + ownPowerLevel = ownPowerLevel, currentPermissions = currentPermissions, itemsBySection = itemsBySection, hasChanges = hasChanges, diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsState.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsState.kt index f538e0a36f..535f2b4a71 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsState.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsState.kt @@ -18,34 +18,55 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf data class ChangeRoomPermissionsState( + private val ownPowerLevel: Long, val currentPermissions: RoomPowerLevelsValues?, val itemsBySection: ImmutableMap>, val hasChanges: Boolean, val saveAction: AsyncAction, val eventSink: (ChangeRoomPermissionsEvent) -> Unit, ) { + private val ownRole = RoomMember.Role.forPowerLevel(ownPowerLevel) + + // Roles that the user can select based on their own role + val selectableRoles: ImmutableList = when (ownRole) { + is RoomMember.Role.Owner, + RoomMember.Role.Admin -> persistentListOf(SelectableRole.Admin, SelectableRole.Moderator, SelectableRole.Everyone) + RoomMember.Role.Moderator -> persistentListOf(SelectableRole.Moderator, SelectableRole.Everyone) + RoomMember.Role.User -> persistentListOf(SelectableRole.Everyone) + } + fun selectedRoleForType(type: RoomPermissionType): SelectableRole? { - if (currentPermissions == null) return null - val role = when (type) { - RoomPermissionType.BAN -> RoomMember.Role.forPowerLevel(currentPermissions.ban) - RoomPermissionType.INVITE -> RoomMember.Role.forPowerLevel(currentPermissions.invite) - RoomPermissionType.KICK -> RoomMember.Role.forPowerLevel(currentPermissions.kick) - RoomPermissionType.SEND_EVENTS -> RoomMember.Role.forPowerLevel(currentPermissions.eventsDefault) - RoomPermissionType.REDACT_EVENTS -> RoomMember.Role.forPowerLevel(currentPermissions.redactEvents) - RoomPermissionType.ROOM_NAME -> RoomMember.Role.forPowerLevel(currentPermissions.roomName) - RoomPermissionType.ROOM_AVATAR -> RoomMember.Role.forPowerLevel(currentPermissions.roomAvatar) - RoomPermissionType.ROOM_TOPIC -> RoomMember.Role.forPowerLevel(currentPermissions.roomTopic) - RoomPermissionType.SPACE_MANAGE_ROOMS -> RoomMember.Role.forPowerLevel(currentPermissions.spaceChild) - } - return when (role) { + val powerLevel = currentPowerLevelForType(type = type) ?: return null + return when (RoomMember.Role.forPowerLevel(powerLevel)) { is RoomMember.Role.Owner, RoomMember.Role.Admin -> SelectableRole.Admin RoomMember.Role.Moderator -> SelectableRole.Moderator RoomMember.Role.User -> SelectableRole.Everyone } } + + fun canChangePermission(type: RoomPermissionType): Boolean { + val currentPowerLevel = currentPowerLevelForType(type) ?: return false + return ownPowerLevel >= currentPowerLevel + } + + private fun currentPowerLevelForType(type: RoomPermissionType): Long? { + if (currentPermissions == null) return null + return when (type) { + RoomPermissionType.BAN -> currentPermissions.ban + RoomPermissionType.INVITE -> currentPermissions.invite + RoomPermissionType.KICK -> currentPermissions.kick + RoomPermissionType.SEND_EVENTS -> currentPermissions.eventsDefault + RoomPermissionType.REDACT_EVENTS -> currentPermissions.redactEvents + RoomPermissionType.ROOM_NAME -> currentPermissions.roomName + RoomPermissionType.ROOM_AVATAR -> currentPermissions.roomAvatar + RoomPermissionType.ROOM_TOPIC -> currentPermissions.roomTopic + RoomPermissionType.SPACE_MANAGE_ROOMS -> currentPermissions.spaceChild + } + } } enum class RoomPermissionsSection { diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt index 6c4e5aa379..2760272d8a 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt @@ -19,6 +19,7 @@ class ChangeRoomPermissionsStateProvider : PreviewParameterProvider get() = sequenceOf( aChangeRoomPermissionsState(), + aChangeRoomPermissionsState(ownPowerLevel = RoomMember.Role.Moderator.powerLevel), aChangeRoomPermissionsState(hasChanges = true), aChangeRoomPermissionsState(hasChanges = true, saveAction = AsyncAction.Loading), aChangeRoomPermissionsState( @@ -31,12 +32,14 @@ class ChangeRoomPermissionsStateProvider : PreviewParameterProvider> = ChangeRoomPermissionsPresenter.buildItems(false), hasChanges: Boolean = false, saveAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (ChangeRoomPermissionsEvent) -> Unit = {}, ) = ChangeRoomPermissionsState( + ownPowerLevel = ownPowerLevel, currentPermissions = currentPermissions, itemsBySection = itemsBySection.toImmutableMap(), hasChanges = hasChanges, diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt index c9d9ed435f..b7084acf57 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt @@ -74,7 +74,8 @@ fun ChangeRoomPermissionsView( PreferenceDropdown( title = titleForType(permissionType), selectedOption = state.selectedRoleForType(permissionType), - options = SelectableRole.entries.toImmutableList(), + options = state.selectableRoles, + enabled = state.canChangePermission(permissionType), onSelectOption = { role -> state.eventSink( ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction( diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenterTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenterTest.kt index 9d94c4831e..34b1ac0da5 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenterTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenterTest.kt @@ -16,13 +16,18 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember.Role.Admin import io.element.android.libraries.matrix.api.room.RoomMember.Role.Moderator +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevelValues import io.element.android.services.analytics.test.FakeAnalyticsService +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.test.runTest import org.junit.Test @@ -70,6 +75,28 @@ class ChangeRoomPermissionsPresenterTest { } } + @Test + fun `present - check canChangePermissions and selectableOptions for moderator`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + initialRoomInfo = initialRoomInfo(role = Moderator), + powerLevelsResult = { Result.success(defaultPermissions()) } + ), + ) + val presenter = createChangeRoomPermissionsPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitUpdatedItem() + assertThat(state.selectableRoles).containsExactly(SelectableRole.Moderator, SelectableRole.Everyone) + for (sectionItems in state.itemsBySection.values) { + for (permissionType in sectionItems) { + assertThat(state.canChangePermission(permissionType)).isTrue() + } + } + } + } + @Test fun `present - ChangeMinimumRoleForAction updates the current permissions and hasChanges`() = runTest { val presenter = createChangeRoomPermissionsPresenter() @@ -266,7 +293,9 @@ class ChangeRoomPermissionsPresenterTest { private fun createChangeRoomPermissionsPresenter( room: FakeJoinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom(powerLevelsResult = { Result.success(defaultPermissions()) }), + baseRoom = FakeBaseRoom( + initialRoomInfo = initialRoomInfo(), + powerLevelsResult = { Result.success(defaultPermissions()) }), ), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ) = ChangeRoomPermissionsPresenter( @@ -274,6 +303,13 @@ class ChangeRoomPermissionsPresenterTest { analyticsService = analyticsService, ) + private fun initialRoomInfo(role: RoomMember.Role = Admin) = aRoomInfo( + roomPowerLevels = RoomPowerLevels( + values = defaultPermissions(), + users = persistentMapOf(A_SESSION_ID to role.powerLevel), + ) + ) + private fun defaultPermissions() = defaultRoomPowerLevelValues() private suspend fun TurbineTestContext.awaitUpdatedItem(): ChangeRoomPermissionsState {