Change a room's permissions power levels (#2525)

* Change a room's permissions power levels

* Make `currentPermissions` use a `MatrixRoomPowerLevels?` instance instead.

* Update strings

* Update screenshots

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-03-12 15:45:06 +01:00 committed by GitHub
parent 796bdc0224
commit d19b3fe091
71 changed files with 1556 additions and 58 deletions

View file

@ -21,5 +21,6 @@ import io.element.android.libraries.matrix.api.room.RoomMember
sealed interface RolesAndPermissionsEvents {
data object ChangeOwnRole : RolesAndPermissionsEvents
data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents
data object ResetPermissions : RolesAndPermissionsEvents
data object CancelPendingAction : RolesAndPermissionsEvents
}

View file

@ -28,6 +28,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesNode
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsNode
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
@ -55,6 +57,9 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
@Parcelize
data object ModeratorList : NavTarget
@Parcelize
data class ChangeRoomPermissions(val section: ChangeRoomPermissionsSection) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -68,6 +73,18 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
override fun openModeratorList() {
backstack.push(NavTarget.ModeratorList)
}
override fun openEditRoomDetailsPermissions() {
backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.RoomDetails))
}
override fun openMessagesAndContentPermissions() {
backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.MessagesAndContent))
}
override fun openModerationPermissions() {
backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.MembershipModeration))
}
}
createNode<RolesAndPermissionsNode>(
buildContext = buildContext,
@ -88,6 +105,13 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
plugins = listOf(inputs),
)
}
is NavTarget.ChangeRoomPermissions -> {
val inputs = ChangeRoomPermissionsNode.Inputs(navTarget.section)
createNode<ChangeRoomPermissionsNode>(
buildContext = buildContext,
plugins = listOf(inputs),
)
}
}
}

View file

@ -17,6 +17,7 @@
package io.element.android.features.roomdetails.impl.rolesandpermissions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@ -46,17 +47,24 @@ class RolesAndPermissionsNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
private val presenter: RolesAndPermissionsPresenter,
private val room: MatrixRoom,
) : Node(buildContext, plugins = plugins), RoomDetailsAdminSettingsNavigator {
interface Callback : Plugin {
fun openAdminList()
fun openModeratorList()
) : Node(buildContext, plugins = plugins), RolesAndPermissionsNavigator {
interface Callback : Plugin, RolesAndPermissionsNavigator {
override fun openAdminList()
override fun openModeratorList()
override fun openEditRoomDetailsPermissions()
override fun openMessagesAndContentPermissions()
override fun openModerationPermissions()
override fun onBackPressed() {}
}
private val callback = plugins<Callback>().first()
override fun onBackPressed() = navigateUp()
override fun openAdminList() = callback.openAdminList()
override fun openModeratorList() = callback.openModeratorList()
@Stable
private val navigator = object : RolesAndPermissionsNavigator by callback {
override fun onBackPressed() {
navigateUp()
}
}
override fun onBuilt() {
super.onBuilt()
@ -88,14 +96,17 @@ class RolesAndPermissionsNode @AssistedInject constructor(
val state = presenter.present()
RolesAndPermissionsView(
state = state,
roomDetailsAdminSettingsNavigator = this,
rolesAndPermissionsNavigator = navigator,
modifier = modifier,
)
}
}
interface RoomDetailsAdminSettingsNavigator {
interface RolesAndPermissionsNavigator {
fun onBackPressed() {}
fun openAdminList() {}
fun openModeratorList() {}
fun openEditRoomDetailsPermissions() {}
fun openMessagesAndContentPermissions() {}
fun openModerationPermissions() {}
}

View file

@ -55,6 +55,7 @@ class RolesAndPermissionsPresenter @Inject constructor(
}
}
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val resetPermissionsAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
fun handleEvent(event: RolesAndPermissionsEvents) {
when (event) {
@ -63,11 +64,17 @@ class RolesAndPermissionsPresenter @Inject constructor(
}
is RolesAndPermissionsEvents.CancelPendingAction -> {
changeOwnRoleAction.value = AsyncAction.Uninitialized
resetPermissionsAction.value = AsyncAction.Uninitialized
}
is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo(
role = event.role,
changeOwnRoleAction = changeOwnRoleAction,
)
is RolesAndPermissionsEvents.ResetPermissions -> if (resetPermissionsAction.value.isConfirming()) {
coroutineScope.resetPermissions(resetPermissionsAction)
} else {
resetPermissionsAction.value = AsyncAction.Confirming
}
}
}
@ -75,6 +82,7 @@ class RolesAndPermissionsPresenter @Inject constructor(
adminCount = adminCount,
moderatorCount = moderatorCount,
changeOwnRoleAction = changeOwnRoleAction.value,
resetPermissionsAction = resetPermissionsAction.value,
eventSink = { handleEvent(it) },
)
}
@ -88,6 +96,14 @@ class RolesAndPermissionsPresenter @Inject constructor(
}
}
private fun CoroutineScope.resetPermissions(
resetPermissionsAction: MutableState<AsyncAction<Unit>>,
) = launch(dispatchers.io) {
runUpdatingState(resetPermissionsAction) {
room.resetPowerLevels().map {}
}
}
private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int {
return if (this != null) {
userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role }

View file

@ -22,5 +22,6 @@ data class RolesAndPermissionsState(
val adminCount: Int,
val moderatorCount: Int,
val changeOwnRoleAction: AsyncAction<Unit>,
val resetPermissionsAction: AsyncAction<Unit>,
val eventSink: (RolesAndPermissionsEvents) -> Unit,
)

View file

@ -39,6 +39,21 @@ class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermis
moderatorCount = 2,
changeOwnRoleAction = AsyncAction.Failure(IllegalStateException("Failed to change role")),
),
aRolesAndPermissionsState(
adminCount = 1,
moderatorCount = 2,
resetPermissionsAction = AsyncAction.Confirming,
),
aRolesAndPermissionsState(
adminCount = 1,
moderatorCount = 2,
resetPermissionsAction = AsyncAction.Loading,
),
aRolesAndPermissionsState(
adminCount = 1,
moderatorCount = 2,
resetPermissionsAction = AsyncAction.Failure(IllegalStateException("Failed to reset permissions")),
),
)
}
@ -46,10 +61,12 @@ internal fun aRolesAndPermissionsState(
adminCount: Int = 0,
moderatorCount: Int = 0,
changeOwnRoleAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
resetPermissionsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
) = RolesAndPermissionsState(
adminCount = adminCount,
moderatorCount = moderatorCount,
changeOwnRoleAction = changeOwnRoleAction,
resetPermissionsAction = resetPermissionsAction,
eventSink = eventSink,
)

View file

@ -33,6 +33,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
@ -53,35 +55,72 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RolesAndPermissionsView(
state: RolesAndPermissionsState,
roomDetailsAdminSettingsNavigator: RoomDetailsAdminSettingsNavigator,
rolesAndPermissionsNavigator: RolesAndPermissionsNavigator,
modifier: Modifier = Modifier,
) {
PreferencePage(
modifier = modifier,
title = stringResource(R.string.screen_room_roles_and_permissions_title),
onBackPressed = roomDetailsAdminSettingsNavigator::onBackPressed,
onBackPressed = rolesAndPermissionsNavigator::onBackPressed,
) {
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_roles_header), hasDivider = false)
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_admins)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
trailingContent = ListItemContent.Text("${state.adminCount}"),
onClick = { roomDetailsAdminSettingsNavigator.openAdminList() },
onClick = { rolesAndPermissionsNavigator.openAdminList() },
)
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_moderators)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
trailingContent = ListItemContent.Text("${state.moderatorCount}"),
onClick = { roomDetailsAdminSettingsNavigator.openModeratorList() },
onClick = { rolesAndPermissionsNavigator.openModeratorList() },
)
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit()))
)
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_permissions_header), hasDivider = true)
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_room_details)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
onClick = { rolesAndPermissionsNavigator.openEditRoomDetailsPermissions() },
)
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_messages_and_content)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Chat())),
onClick = { rolesAndPermissionsNavigator.openMessagesAndContentPermissions() },
)
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_member_moderation)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
onClick = { rolesAndPermissionsNavigator.openModerationPermissions() },
)
HorizontalDivider()
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_reset)) },
onClick = { state.eventSink(RolesAndPermissionsEvents.ResetPermissions) },
style = ListItemStyle.Destructive,
)
}
AsyncActionView(
async = state.resetPermissionsAction,
confirmationDialog = {
ConfirmationDialog(
title = stringResource(R.string.screen_room_roles_and_permissions_reset_confirm_title),
content = stringResource(R.string.screen_room_roles_and_permissions_reset_confirm_description),
submitText = stringResource(CommonStrings.action_reset),
destructiveSubmit = true,
onSubmitClicked = { state.eventSink(RolesAndPermissionsEvents.ResetPermissions) },
onDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) },
)
},
onSuccess = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) },
onErrorDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) }
)
when (state.changeOwnRoleAction) {
is AsyncAction.Confirming -> {
ChangeOwnRoleBottomSheet(
@ -156,11 +195,7 @@ private fun ChangeOwnRoleBottomSheet(
)
ListItem(
headlineContent = { Text(stringResource(CommonStrings.action_cancel)) },
onClick = {
sheetState.hide(coroutineScope) {
eventSink(RolesAndPermissionsEvents.CancelPendingAction)
}
},
onClick = ::dismiss,
style = ListItemStyle.Primary,
)
}
@ -168,11 +203,11 @@ private fun ChangeOwnRoleBottomSheet(
@PreviewsDayNight
@Composable
internal fun RoomDetailsAdminSettingsViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
internal fun RolesAndPermissionViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
ElementPreview {
RolesAndPermissionsView(
state = state,
roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {},
rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator {},
)
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
import io.element.android.libraries.matrix.api.room.RoomMember
interface ChangeRoomPermissionsEvent {
data class ChangeMinimumRoleForAction(val action: RoomPermissionType, val role: RoomMember.Role) : ChangeRoomPermissionsEvent
data object Save : ChangeRoomPermissionsEvent
data object Exit : ChangeRoomPermissionsEvent
data object ResetPendingActions : ChangeRoomPermissionsEvent
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
class ChangeRoomPermissionsNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: ChangeRoomPermissionsPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
@Parcelize
data class Inputs(
val section: ChangeRoomPermissionsSection,
) : NodeInputs, Parcelable
private val inputs: Inputs = inputs()
private val presenter = presenterFactory.run {
create(inputs.section)
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
ChangeRoomPermissionsView(
modifier = modifier,
state = state,
onBackPressed = this::navigateUp,
)
}
}
@Parcelize
enum class ChangeRoomPermissionsSection : Parcelable {
RoomDetails,
MessagesAndContent,
MembershipModeration,
}

View file

@ -0,0 +1,145 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class ChangeRoomPermissionsPresenter @AssistedInject constructor(
@Assisted private val section: ChangeRoomPermissionsSection,
private val room: MatrixRoom,
) : Presenter<ChangeRoomPermissionsState> {
companion object {
internal fun itemsForSection(section: ChangeRoomPermissionsSection) = when (section) {
ChangeRoomPermissionsSection.RoomDetails -> persistentListOf(
RoomPermissionType.ROOM_NAME,
RoomPermissionType.ROOM_AVATAR,
RoomPermissionType.ROOM_TOPIC,
)
ChangeRoomPermissionsSection.MessagesAndContent -> persistentListOf(
RoomPermissionType.SEND_EVENTS,
RoomPermissionType.REDACT_EVENTS,
)
ChangeRoomPermissionsSection.MembershipModeration -> persistentListOf(
RoomPermissionType.INVITE,
RoomPermissionType.KICK,
RoomPermissionType.BAN,
)
}
}
@AssistedFactory
interface Factory {
fun create(section: ChangeRoomPermissionsSection): ChangeRoomPermissionsPresenter
}
private val items: ImmutableList<RoomPermissionType> = itemsForSection(section)
private var initialPermissions by mutableStateOf<MatrixRoomPowerLevels?>(null)
private var currentPermissions by mutableStateOf<MatrixRoomPowerLevels?>(null)
private var saveAction by mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
private var confirmExitAction by mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
@Composable
override fun present(): ChangeRoomPermissionsState {
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
updatePermissions()
}
val hasChanges by remember {
derivedStateOf { initialPermissions != currentPermissions }
}
fun handleEvent(event: ChangeRoomPermissionsEvent) {
when (event) {
is ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction -> {
currentPermissions = when (event.action) {
RoomPermissionType.BAN -> currentPermissions?.copy(ban = event.role.powerLevel)
RoomPermissionType.INVITE -> currentPermissions?.copy(invite = event.role.powerLevel)
RoomPermissionType.KICK -> currentPermissions?.copy(kick = event.role.powerLevel)
RoomPermissionType.SEND_EVENTS -> currentPermissions?.copy(sendEvents = event.role.powerLevel)
RoomPermissionType.REDACT_EVENTS -> currentPermissions?.copy(redactEvents = event.role.powerLevel)
RoomPermissionType.ROOM_NAME -> currentPermissions?.copy(roomName = event.role.powerLevel)
RoomPermissionType.ROOM_AVATAR -> currentPermissions?.copy(roomAvatar = event.role.powerLevel)
RoomPermissionType.ROOM_TOPIC -> currentPermissions?.copy(roomTopic = event.role.powerLevel)
}
}
is ChangeRoomPermissionsEvent.Save -> coroutineScope.save()
is ChangeRoomPermissionsEvent.Exit -> {
confirmExitAction = if (!hasChanges || confirmExitAction.isConfirming()) {
AsyncAction.Success(Unit)
} else {
AsyncAction.Confirming
}
}
is ChangeRoomPermissionsEvent.ResetPendingActions -> {
saveAction = AsyncAction.Uninitialized
confirmExitAction = AsyncAction.Uninitialized
}
}
}
return ChangeRoomPermissionsState(
section = section,
currentPermissions = currentPermissions,
items = items,
hasChanges = hasChanges,
saveAction = saveAction,
confirmExitAction = confirmExitAction,
eventSink = { handleEvent(it) }
)
}
private suspend fun updatePermissions() {
val powerLevels = room.powerLevels().getOrNull() ?: return
initialPermissions = powerLevels
currentPermissions = initialPermissions
}
private fun CoroutineScope.save() = launch {
saveAction = AsyncAction.Loading
val updatedRoomPowerLevels = currentPermissions ?: run {
saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels"))
return@launch
}
room.updatePowerLevels(updatedRoomPowerLevels)
.onSuccess {
initialPermissions = currentPermissions
saveAction = AsyncAction.Success(Unit)
}
.onFailure {
saveAction = AsyncAction.Failure(it)
}
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import kotlinx.collections.immutable.ImmutableList
data class ChangeRoomPermissionsState(
val section: ChangeRoomPermissionsSection,
val currentPermissions: MatrixRoomPowerLevels?,
val items: ImmutableList<RoomPermissionType>,
val hasChanges: Boolean,
val saveAction: AsyncAction<Unit>,
val confirmExitAction: AsyncAction<Unit>,
val eventSink: (ChangeRoomPermissionsEvent) -> Unit,
)
enum class RoomPermissionType {
BAN,
INVITE,
KICK,
SEND_EVENTS,
REDACT_EVENTS,
ROOM_NAME,
ROOM_AVATAR,
ROOM_TOPIC
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import kotlinx.collections.immutable.toPersistentList
class ChangeRoomPermissionsStatePreviewProvider : PreviewParameterProvider<ChangeRoomPermissionsState> {
override val values: Sequence<ChangeRoomPermissionsState>
get() = sequenceOf(
aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.RoomDetails),
aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.MessagesAndContent),
aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.MembershipModeration),
aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.RoomDetails, hasChanges = true),
aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.RoomDetails, hasChanges = true, saveAction = AsyncAction.Loading),
aChangeRoomPermissionsState(
section = ChangeRoomPermissionsSection.RoomDetails,
hasChanges = true,
saveAction = AsyncAction.Failure(IllegalStateException("Failed to save changes"))
),
aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.RoomDetails, hasChanges = true, confirmExitAction = AsyncAction.Confirming),
)
}
internal fun aChangeRoomPermissionsState(
section: ChangeRoomPermissionsSection,
currentPermissions: MatrixRoomPowerLevels = previewPermissions(),
items: List<RoomPermissionType> = ChangeRoomPermissionsPresenter.itemsForSection(section),
hasChanges: Boolean = false,
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
confirmExitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (ChangeRoomPermissionsEvent) -> Unit = {},
) = ChangeRoomPermissionsState(
section = section,
currentPermissions = currentPermissions,
items = items.toPersistentList(),
hasChanges = hasChanges,
saveAction = saveAction,
confirmExitAction = confirmExitAction,
eventSink = eventSink,
)
private fun previewPermissions(): MatrixRoomPowerLevels {
return MatrixRoomPowerLevels(
// MembershipModeration section
invite = RoomMember.Role.ADMIN.powerLevel,
kick = RoomMember.Role.MODERATOR.powerLevel,
ban = RoomMember.Role.USER.powerLevel,
// MessagesAndContent section
redactEvents = RoomMember.Role.MODERATOR.powerLevel,
sendEvents = RoomMember.Role.ADMIN.powerLevel,
// RoomDetails section
roomName = RoomMember.Role.ADMIN.powerLevel,
roomAvatar = RoomMember.Role.MODERATOR.powerLevel,
roomTopic = RoomMember.Role.USER.powerLevel,
)
}

View file

@ -0,0 +1,192 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChangeRoomPermissionsView(
state: ChangeRoomPermissionsState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler {
state.eventSink(ChangeRoomPermissionsEvent.Exit)
}
Scaffold(
modifier = modifier,
topBar = {
val title = when (state.section) {
ChangeRoomPermissionsSection.RoomDetails -> stringResource(R.string.screen_room_change_permissions_room_details)
ChangeRoomPermissionsSection.MessagesAndContent -> stringResource(R.string.screen_room_change_permissions_messages_and_content)
ChangeRoomPermissionsSection.MembershipModeration -> stringResource(R.string.screen_room_change_permissions_member_moderation)
}
TopAppBar(
title = { Text(text = title, style = ElementTheme.typography.aliasScreenTitle) },
navigationIcon = {
BackButton(onClick = { state.eventSink(ChangeRoomPermissionsEvent.Exit) })
},
actions = {
TextButton(
text = stringResource(CommonStrings.action_save),
onClick = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
enabled = state.hasChanges,
)
}
)
}
) { padding ->
Column(modifier = Modifier.padding(padding)) {
for ((index, permissionItem) in state.items.withIndex()) {
ListSectionHeader(titleForSection(item = permissionItem), hasDivider = index > 0)
SelectRoleItem(
permissionsItem = permissionItem,
role = RoomMember.Role.ADMIN,
currentPermissions = state.currentPermissions
) { item, role ->
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
}
SelectRoleItem(
permissionsItem = permissionItem,
role = RoomMember.Role.MODERATOR,
currentPermissions = state.currentPermissions
) { item, role ->
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
}
SelectRoleItem(
permissionsItem = permissionItem,
role = RoomMember.Role.USER,
currentPermissions = state.currentPermissions
) { item, role ->
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
}
}
}
}
AsyncActionView(
async = state.saveAction,
onSuccess = { onBackPressed() },
onErrorDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) }
)
AsyncActionView(
async = state.confirmExitAction,
onSuccess = { onBackPressed() },
confirmationDialog = {
ConfirmationDialog(
title = stringResource(R.string.screen_room_change_role_unsaved_changes_title),
content = stringResource(R.string.screen_room_change_role_unsaved_changes_description),
submitText = stringResource(CommonStrings.action_save),
cancelText = stringResource(CommonStrings.action_discard),
onSubmitClicked = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
onDismiss = { state.eventSink(ChangeRoomPermissionsEvent.Exit) }
)
},
onErrorDismiss = {},
)
}
@Composable
private fun SelectRoleItem(
permissionsItem: RoomPermissionType,
role: RoomMember.Role,
currentPermissions: MatrixRoomPowerLevels?,
onClick: (RoomPermissionType, RoomMember.Role) -> Unit
) {
val title = when (role) {
RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_change_permissions_administrators)
RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_change_permissions_moderators)
RoomMember.Role.USER -> stringResource(R.string.screen_room_change_permissions_everyone)
}
ListItem(
headlineContent = { Text(text = title) },
trailingContent = if (currentPermissions?.isSelected(permissionsItem, role).orFalse()) {
ListItemContent.Icon(IconSource.Vector(CompoundIcons.Check()))
} else {
null
},
style = ListItemStyle.Primary,
onClick = { onClick(permissionsItem, role) },
)
}
private fun MatrixRoomPowerLevels.isSelected(item: RoomPermissionType, role: RoomMember.Role): Boolean {
return when (item) {
RoomPermissionType.BAN -> RoomMember.Role.forPowerLevel(ban) == role
RoomPermissionType.INVITE -> RoomMember.Role.forPowerLevel(invite) == role
RoomPermissionType.KICK -> RoomMember.Role.forPowerLevel(kick) == role
RoomPermissionType.SEND_EVENTS -> RoomMember.Role.forPowerLevel(sendEvents) == role
RoomPermissionType.REDACT_EVENTS -> RoomMember.Role.forPowerLevel(redactEvents) == role
RoomPermissionType.ROOM_NAME -> RoomMember.Role.forPowerLevel(roomName) == role
RoomPermissionType.ROOM_AVATAR -> RoomMember.Role.forPowerLevel(roomAvatar) == role
RoomPermissionType.ROOM_TOPIC -> RoomMember.Role.forPowerLevel(roomTopic) == role
}
}
@Composable
private fun titleForSection(item: RoomPermissionType): String = when (item) {
RoomPermissionType.INVITE -> stringResource(R.string.screen_room_change_permissions_invite_people)
RoomPermissionType.KICK -> stringResource(R.string.screen_room_change_permissions_remove_people)
RoomPermissionType.BAN -> stringResource(R.string.screen_room_change_permissions_ban_people)
RoomPermissionType.SEND_EVENTS -> stringResource(R.string.screen_room_change_permissions_send_messages)
RoomPermissionType.REDACT_EVENTS -> stringResource(R.string.screen_room_change_permissions_delete_messages)
RoomPermissionType.ROOM_NAME -> stringResource(R.string.screen_room_change_permissions_room_name)
RoomPermissionType.ROOM_AVATAR -> stringResource(R.string.screen_room_change_permissions_room_avatar)
RoomPermissionType.ROOM_TOPIC -> stringResource(R.string.screen_room_change_permissions_room_topic)
}
@PreviewsDayNight
@Composable
internal fun ChangeRoomPermissionsViewPreview(@PreviewParameter(ChangeRoomPermissionsStatePreviewProvider::class) state: ChangeRoomPermissionsState) {
ElementPreview {
ChangeRoomPermissionsView(
state = state,
onBackPressed = {},
)
}
}

View file

@ -18,10 +18,10 @@
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
<string name="screen_room_change_permissions_moderators">"Admins and moderators"</string>
<string name="screen_room_change_permissions_remove_people">"Remove people"</string>
<string name="screen_room_change_permissions_room_avatar">"Change Room Avatar"</string>
<string name="screen_room_change_permissions_room_avatar">"Change room avatar"</string>
<string name="screen_room_change_permissions_room_details">"Room details"</string>
<string name="screen_room_change_permissions_room_name">"Change Room Name"</string>
<string name="screen_room_change_permissions_room_topic">"Change Room Topic"</string>
<string name="screen_room_change_permissions_room_name">"Change room name"</string>
<string name="screen_room_change_permissions_room_topic">"Change room topic"</string>
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
@ -104,7 +104,7 @@
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose your current settings."</string>
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose the current settings."</string>
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Reset permissions?"</string>
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>