change(room permissions): user can edit only roles <= to his own role

This commit is contained in:
ganfra
2025-12-19 17:10:28 +01:00
parent 74dd3f381e
commit f13d9259c5
5 changed files with 84 additions and 15 deletions

View File

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

View File

@@ -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<RoomPermissionsSection, ImmutableList<RoomPermissionType>>,
val hasChanges: Boolean,
val saveAction: AsyncAction<Boolean>,
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<SelectableRole> = 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 {

View File

@@ -19,6 +19,7 @@ class ChangeRoomPermissionsStateProvider : PreviewParameterProvider<ChangeRoomPe
override val values: Sequence<ChangeRoomPermissionsState>
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<ChangeRoomPe
}
internal fun aChangeRoomPermissionsState(
ownPowerLevel: Long = RoomMember.Role.Admin.powerLevel,
currentPermissions: RoomPowerLevelsValues = previewPermissions(),
itemsBySection: Map<RoomPermissionsSection, ImmutableList<RoomPermissionType>> = ChangeRoomPermissionsPresenter.buildItems(false),
hasChanges: Boolean = false,
saveAction: AsyncAction<Boolean> = AsyncAction.Uninitialized,
eventSink: (ChangeRoomPermissionsEvent) -> Unit = {},
) = ChangeRoomPermissionsState(
ownPowerLevel = ownPowerLevel,
currentPermissions = currentPermissions,
itemsBySection = itemsBySection.toImmutableMap(),
hasChanges = hasChanges,

View File

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

View File

@@ -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<ChangeRoomPermissionsState>.awaitUpdatedItem(): ChangeRoomPermissionsState {