Merge pull request #5950 from element-hq/feature/fga/iterate_permissions_screen
Changes : iterate again on permissions
This commit is contained in:
commit
76bc487f28
36 changed files with 243 additions and 151 deletions
|
|
@ -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
|
||||
|
|
@ -52,7 +55,6 @@ class ChangeRoomPermissionsPresenter(
|
|||
)
|
||||
RoomPermissionsSection.ManageSpace -> persistentListOf(
|
||||
RoomPermissionType.SPACE_MANAGE_ROOMS,
|
||||
RoomPermissionType.CHANGE_SETTINGS,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -90,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 -> {
|
||||
|
|
@ -108,7 +114,6 @@ class ChangeRoomPermissionsPresenter(
|
|||
RoomPermissionType.ROOM_AVATAR -> currentPermissions?.copy(roomAvatar = powerLevel)
|
||||
RoomPermissionType.ROOM_TOPIC -> currentPermissions?.copy(roomTopic = powerLevel)
|
||||
RoomPermissionType.SPACE_MANAGE_ROOMS -> currentPermissions?.copy(spaceChild = powerLevel)
|
||||
RoomPermissionType.CHANGE_SETTINGS -> currentPermissions?.copy(stateDefault = powerLevel)
|
||||
}
|
||||
}
|
||||
is ChangeRoomPermissionsEvent.Save -> coroutineScope.save()
|
||||
|
|
@ -125,6 +130,7 @@ class ChangeRoomPermissionsPresenter(
|
|||
}
|
||||
}
|
||||
return ChangeRoomPermissionsState(
|
||||
ownPowerLevel = ownPowerLevel,
|
||||
currentPermissions = currentPermissions,
|
||||
itemsBySection = itemsBySection,
|
||||
hasChanges = hasChanges,
|
||||
|
|
|
|||
|
|
@ -18,35 +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)
|
||||
RoomPermissionType.CHANGE_SETTINGS -> RoomMember.Role.forPowerLevel(currentPermissions.stateDefault)
|
||||
}
|
||||
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 {
|
||||
|
|
@ -84,5 +104,4 @@ enum class RoomPermissionType {
|
|||
ROOM_AVATAR,
|
||||
ROOM_TOPIC,
|
||||
SPACE_MANAGE_ROOMS,
|
||||
CHANGE_SETTINGS,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -74,7 +73,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(
|
||||
|
|
@ -127,7 +127,6 @@ private fun titleForType(type: RoomPermissionType): String = when (type) {
|
|||
RoomPermissionType.ROOM_AVATAR -> stringResource(R.string.screen_room_change_permissions_room_avatar)
|
||||
RoomPermissionType.ROOM_TOPIC -> stringResource(R.string.screen_room_change_permissions_room_topic)
|
||||
RoomPermissionType.SPACE_MANAGE_ROOMS -> stringResource(R.string.screen_room_change_permissions_manage_space_rooms)
|
||||
RoomPermissionType.CHANGE_SETTINGS -> stringResource(R.string.screen_room_change_permissions_change_settings)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
|||
import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
|
||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.powerLevelOf
|
||||
import io.element.android.libraries.matrix.ui.model.roleOf
|
||||
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
|
@ -124,9 +125,10 @@ class ChangeRolesPresenter(
|
|||
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
fun canChangeMemberRole(userId: UserId): Boolean {
|
||||
val currentUserRole = roomInfo.roleOf(room.sessionId)
|
||||
val otherUserRole = roomInfo.roleOf(userId)
|
||||
return currentUserRole.powerLevel > otherUserRole.powerLevel
|
||||
val currentUserPowerLevel = roomInfo.powerLevelOf(room.sessionId)
|
||||
val otherUserPowerLevel = roomInfo.powerLevelOf(userId)
|
||||
return currentUserPowerLevel > otherUserPowerLevel &&
|
||||
currentUserPowerLevel >= role.powerLevel
|
||||
}
|
||||
|
||||
fun handleEvent(event: ChangeRolesEvent) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
|||
import io.element.android.libraries.matrix.api.room.powerlevels.userCountWithRole
|
||||
import io.element.android.libraries.matrix.ui.model.roleOf
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -49,7 +50,16 @@ class RolesAndPermissionsPresenter(
|
|||
room.userCountWithRole { role -> role is RoomMember.Role.Admin || role is RoomMember.Role.Owner }
|
||||
}.collectAsState(null)
|
||||
|
||||
val canDemoteSelf = remember { derivedStateOf { roomInfo.roleOf(room.sessionId) !is RoomMember.Role.Owner } }
|
||||
val availableDemoteActions by remember {
|
||||
derivedStateOf {
|
||||
val currentRole = roomInfo.roleOf(room.sessionId)
|
||||
when (currentRole) {
|
||||
is RoomMember.Role.Admin -> persistentListOf(SelfDemoteAction.ToModerator, SelfDemoteAction.ToMember)
|
||||
is RoomMember.Role.Moderator -> persistentListOf(SelfDemoteAction.ToMember)
|
||||
else -> persistentListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
val resetPermissionsAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
|
|
@ -78,7 +88,7 @@ class RolesAndPermissionsPresenter(
|
|||
roomSupportsOwnerRole = roomInfo.privilegedCreatorRole,
|
||||
adminCount = adminCount,
|
||||
moderatorCount = moderatorCount,
|
||||
canDemoteSelf = canDemoteSelf.value,
|
||||
availableSelfDemoteActions = availableDemoteActions,
|
||||
changeOwnRoleAction = changeOwnRoleAction.value,
|
||||
resetPermissionsAction = resetPermissionsAction.value,
|
||||
eventSink = ::handleEvent,
|
||||
|
|
|
|||
|
|
@ -8,14 +8,24 @@
|
|||
|
||||
package io.element.android.features.rolesandpermissions.impl.root
|
||||
|
||||
import io.element.android.features.rolesandpermissions.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RolesAndPermissionsState(
|
||||
val roomSupportsOwnerRole: Boolean,
|
||||
val adminCount: Int?,
|
||||
val moderatorCount: Int?,
|
||||
val canDemoteSelf: Boolean,
|
||||
val availableSelfDemoteActions: ImmutableList<SelfDemoteAction>,
|
||||
val changeOwnRoleAction: AsyncAction<Unit>,
|
||||
val resetPermissionsAction: AsyncAction<Unit>,
|
||||
val eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
)
|
||||
) {
|
||||
val canSelfDemote = availableSelfDemoteActions.isNotEmpty()
|
||||
}
|
||||
|
||||
enum class SelfDemoteAction(val role: RoomMember.Role, val titleRes: Int) {
|
||||
ToModerator(RoomMember.Role.Moderator, R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator),
|
||||
ToMember(RoomMember.Role.User, R.string.screen_room_roles_and_permissions_change_role_demote_to_member)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.rolesandpermissions.impl.root
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermissionsState> {
|
||||
override val values: Sequence<RolesAndPermissionsState>
|
||||
|
|
@ -46,7 +47,7 @@ class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermis
|
|||
moderatorCount = 2,
|
||||
resetPermissionsAction = AsyncAction.Failure(IllegalStateException("Failed to reset permissions")),
|
||||
),
|
||||
aRolesAndPermissionsState(canDemoteSelf = false),
|
||||
aRolesAndPermissionsState(availableSelfDemoteActions = emptyList()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -54,14 +55,14 @@ internal fun aRolesAndPermissionsState(
|
|||
roomSupportsOwners: Boolean = true,
|
||||
adminCount: Int = 0,
|
||||
moderatorCount: Int = 0,
|
||||
canDemoteSelf: Boolean = true,
|
||||
availableSelfDemoteActions: List<SelfDemoteAction> = listOf(SelfDemoteAction.ToModerator, SelfDemoteAction.ToMember),
|
||||
changeOwnRoleAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
resetPermissionsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
|
||||
) = RolesAndPermissionsState(
|
||||
roomSupportsOwnerRole = roomSupportsOwners,
|
||||
adminCount = adminCount,
|
||||
canDemoteSelf = canDemoteSelf,
|
||||
availableSelfDemoteActions = availableSelfDemoteActions.toImmutableList(),
|
||||
moderatorCount = moderatorCount,
|
||||
changeOwnRoleAction = changeOwnRoleAction,
|
||||
resetPermissionsAction = resetPermissionsAction,
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ import io.element.android.libraries.designsystem.theme.components.ListSectionHea
|
|||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun RolesAndPermissionsView(
|
||||
|
|
@ -76,7 +76,7 @@ fun RolesAndPermissionsView(
|
|||
},
|
||||
onClick = { rolesAndPermissionsNavigator.openModeratorList() },
|
||||
)
|
||||
if (state.canDemoteSelf) {
|
||||
if (state.canSelfDemote) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
|
||||
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
|
||||
|
|
@ -117,6 +117,7 @@ fun RolesAndPermissionsView(
|
|||
when (state.changeOwnRoleAction) {
|
||||
is AsyncAction.Confirming -> {
|
||||
ChangeOwnRoleBottomSheet(
|
||||
availableDemoteActions = state.availableSelfDemoteActions,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
}
|
||||
|
|
@ -136,6 +137,7 @@ fun RolesAndPermissionsView(
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ChangeOwnRoleBottomSheet(
|
||||
availableDemoteActions: ImmutableList<SelfDemoteAction>,
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
|
@ -164,24 +166,17 @@ private fun ChangeOwnRoleBottomSheet(
|
|||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_member)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
for (demoteAction in availableDemoteActions) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(demoteAction.titleRes)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(demoteAction.role))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.action_cancel)) },
|
||||
onClick = ::dismiss,
|
||||
|
|
|
|||
|
|
@ -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,10 @@ 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 +304,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 {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ data class RoomMemberListState(
|
|||
val moderationState: RoomMemberModerationState,
|
||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||
) {
|
||||
val showBannedSection: Boolean = moderationState.permissions.canBan && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true
|
||||
val showBannedSection: Boolean = moderationState.permissions.hasAny && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
enum class SelectedSection {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ data class RoomMemberModerationPermissions(
|
|||
val canKick: Boolean,
|
||||
val canBan: Boolean,
|
||||
) {
|
||||
val hasAny = canKick || canBan
|
||||
|
||||
companion object {
|
||||
val DEFAULT = RoomMemberModerationPermissions(
|
||||
canKick = false,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.architecture.AsyncAction
|
|||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
|
@ -35,7 +36,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState
|
||||
import io.element.android.libraries.matrix.ui.model.powerLevelOf
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -56,11 +57,14 @@ class RoomMemberModerationPresenter(
|
|||
@Composable
|
||||
override fun present(): RoomMemberModerationState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
val permissions by room.permissionsAsState(RoomMemberModerationPermissions.DEFAULT) { perms ->
|
||||
perms.roomMemberModerationPermissions()
|
||||
}
|
||||
val currentUserMemberPowerLevel = room.userPowerLevelAsState(syncUpdateFlow.value)
|
||||
val currentUserPowerLevel by remember {
|
||||
room.roomInfoFlow.mapState { info ->
|
||||
info.powerLevelOf(room.sessionId)
|
||||
}
|
||||
}.collectAsState()
|
||||
|
||||
val kickUserAsyncAction =
|
||||
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
|
|
@ -83,7 +87,7 @@ class RoomMemberModerationPresenter(
|
|||
moderationActions.value = computeModerationActions(
|
||||
member = member,
|
||||
permissions = permissions,
|
||||
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
|
||||
currentUserPowerLevel = currentUserPowerLevel,
|
||||
)
|
||||
}
|
||||
is RoomMemberModerationEvents.ProcessAction -> {
|
||||
|
|
@ -148,26 +152,26 @@ class RoomMemberModerationPresenter(
|
|||
private fun computeModerationActions(
|
||||
member: RoomMember?,
|
||||
permissions: RoomMemberModerationPermissions,
|
||||
currentUserMemberPowerLevel: Long,
|
||||
currentUserPowerLevel: Long,
|
||||
): ImmutableList<ModerationActionState> {
|
||||
return buildList {
|
||||
add(ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true))
|
||||
// Assume the member is a regular user when it's unknown
|
||||
val targetMemberPowerLevel = member?.powerLevel ?: 0
|
||||
val canModerateThisUser = currentUserMemberPowerLevel > targetMemberPowerLevel
|
||||
val canModerateThisUser = currentUserPowerLevel > targetMemberPowerLevel
|
||||
// Assume the member is joined when it's unknown
|
||||
val membership = member?.membership ?: RoomMembershipState.JOIN
|
||||
if (permissions.canKick) {
|
||||
val isKickEnabled = canModerateThisUser && membership.isActive()
|
||||
add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = isKickEnabled))
|
||||
}
|
||||
if (permissions.canBan) {
|
||||
// Unban requires kick permission instead of a dedicated unban permission
|
||||
if (membership == RoomMembershipState.BAN) {
|
||||
add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser))
|
||||
} else {
|
||||
add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser))
|
||||
} else if (membership != RoomMembershipState.LEAVE) {
|
||||
add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = canModerateThisUser))
|
||||
}
|
||||
}
|
||||
if (permissions.canBan && membership != RoomMembershipState.BAN) {
|
||||
add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,11 +21,14 @@ 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.RoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_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.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevelValues
|
||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
|
|
@ -33,6 +36,7 @@ import io.element.android.tests.testutils.WarmUpRule
|
|||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -161,7 +165,6 @@ class RoomMemberModerationPresenterTest {
|
|||
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
|
||||
assertThat(updatedState.actions).containsExactly(
|
||||
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
|
||||
ModerationActionState(action = ModerationAction.KickUser, isEnabled = false),
|
||||
ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = true),
|
||||
)
|
||||
}
|
||||
|
|
@ -223,9 +226,11 @@ class RoomMemberModerationPresenterTest {
|
|||
val room = aJoinedRoom()
|
||||
room.baseRoom.givenUpdateMembersResult {
|
||||
// Simulate the member list being updated
|
||||
room.givenRoomMembersState(RoomMembersState.Ready(
|
||||
persistentListOf(aRoomMember())
|
||||
))
|
||||
room.givenRoomMembersState(
|
||||
RoomMembersState.Ready(
|
||||
persistentListOf(aRoomMember())
|
||||
)
|
||||
)
|
||||
}
|
||||
createRoomMemberModerationPresenter(room = room).test {
|
||||
val initialState = awaitState()
|
||||
|
|
@ -251,9 +256,11 @@ class RoomMemberModerationPresenterTest {
|
|||
val room = aJoinedRoom()
|
||||
room.baseRoom.givenUpdateMembersResult {
|
||||
// Simulate the member list being updated
|
||||
room.givenRoomMembersState(RoomMembersState.Ready(
|
||||
persistentListOf(aRoomMember())
|
||||
))
|
||||
room.givenRoomMembersState(
|
||||
RoomMembersState.Ready(
|
||||
persistentListOf(aRoomMember())
|
||||
)
|
||||
)
|
||||
}
|
||||
createRoomMemberModerationPresenter(room = room).test {
|
||||
val initialState = awaitState()
|
||||
|
|
@ -279,9 +286,11 @@ class RoomMemberModerationPresenterTest {
|
|||
val room = aJoinedRoom()
|
||||
room.baseRoom.givenUpdateMembersResult {
|
||||
// Simulate the member list being updated
|
||||
room.givenRoomMembersState(RoomMembersState.Ready(
|
||||
persistentListOf(aRoomMember())
|
||||
))
|
||||
room.givenRoomMembersState(
|
||||
RoomMembersState.Ready(
|
||||
persistentListOf(aRoomMember())
|
||||
)
|
||||
)
|
||||
}
|
||||
createRoomMemberModerationPresenter(room = room).test {
|
||||
val initialState = awaitState()
|
||||
|
|
@ -361,7 +370,13 @@ class RoomMemberModerationPresenterTest {
|
|||
canKick = canKick
|
||||
),
|
||||
userRoleResult = { Result.success(myUserRole) },
|
||||
updateMembersResult = { Result.success(Unit) }
|
||||
updateMembersResult = { Result.success(Unit) },
|
||||
initialRoomInfo = aRoomInfo(
|
||||
roomPowerLevels = RoomPowerLevels(
|
||||
values = defaultRoomPowerLevelValues(),
|
||||
users = persistentMapOf(A_USER_ID to myUserRole.powerLevel)
|
||||
)
|
||||
)
|
||||
),
|
||||
).apply {
|
||||
val roomMembers = listOfNotNull(targetRoomMember).toImmutableList()
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ private fun PreferenceBlockUser(
|
|||
isLoading: Boolean,
|
||||
eventSink: (UserProfileEvents) -> Unit,
|
||||
) {
|
||||
val loadingCurrentValue = @Composable {
|
||||
val loadingCurrentValue = @Composable { _: Boolean ->
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.progressSemantics()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue