From c6eea91d6932e3289dcc2fc8cc685f2d07a12f71 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 16 May 2025 18:38:15 +0200 Subject: [PATCH] change (member moderation) : allow disabled action and render unban too --- .../features/messages/impl/MessagesNode.kt | 6 +- .../impl/members/RoomMemberListNode.kt | 6 +- .../impl/members/RoomMemberListPresenter.kt | 2 +- .../api/RoomMemberModerationEvents.kt | 2 +- .../api/RoomMemberModerationRenderer.kt | 3 +- .../api/RoomMemberModerationState.kt | 15 ++-- .../DefaultRoomMemberModerationRenderer.kt | 3 +- .../impl/InternalRoomMemberModerationState.kt | 3 +- .../impl/RoomMemberModerationPresenter.kt | 69 ++++++++++--------- .../impl/RoomMemberModerationStateProvider.kt | 23 +++++-- .../impl/RoomMemberModerationView.kt | 64 +++++++++++------ 11 files changed, 119 insertions(+), 77 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 5f2546dd50..6acf48e58b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -263,10 +263,10 @@ class MessagesNode @AssistedInject constructor( ) roomMemberModerationRenderer.Render( state = state.roomMemberModerationState, - onSelectAction = { action -> + onSelectAction = { action, target -> when (action) { - is ModerationAction.DisplayProfile -> onUserDataClick(action.user.userId) - else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action)) + is ModerationAction.DisplayProfile -> onUserDataClick(target.userId) + else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) } }, modifier = Modifier, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index fd9dc1cd51..cf2f89c41e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -75,10 +75,10 @@ class RoomMemberListNode @AssistedInject constructor( ) roomMemberModerationRenderer.Render( state = state.moderationState, - onSelectAction = { action -> + onSelectAction = { action, target -> when (action) { - is ModerationAction.DisplayProfile -> openRoomMemberDetails(action.user.userId) - else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action)) + is ModerationAction.DisplayProfile -> openRoomMemberDetails(target.userId) + else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) } }, modifier = Modifier, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 0066b80231..c637f338cb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -167,7 +167,7 @@ class RoomMemberListPresenter @AssistedInject constructor( is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> if (event.roomMember.membership == RoomMembershipState.BAN) { - roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser(event.roomMember.toMatrixUser()))) + roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser, event.roomMember.toMatrixUser())) } else if (!isDm.value && (roomModerationState.canBan || roomModerationState.canKick)) { roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser())) } else { diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt index 277b4275d6..94c7477739 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt @@ -11,5 +11,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser interface RoomMemberModerationEvents { data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvents - data class ProcessAction(val action: ModerationAction) : RoomMemberModerationEvents + data class ProcessAction(val action: ModerationAction, val targetUser: MatrixUser) : RoomMemberModerationEvents } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt index 2911bf1b8b..afe8ab0f8b 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt @@ -9,12 +9,13 @@ package io.element.android.features.roommembermoderation.api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.element.android.libraries.matrix.api.user.MatrixUser interface RoomMemberModerationRenderer { @Composable fun Render( state: RoomMemberModerationState, - onSelectAction: (ModerationAction) -> Unit, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, modifier: Modifier, ) } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt index b31857b594..51f791ed69 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -7,17 +7,20 @@ package io.element.android.features.roommembermoderation.api -import io.element.android.libraries.matrix.api.user.MatrixUser - interface RoomMemberModerationState { val canKick: Boolean val canBan: Boolean val eventSink: (RoomMemberModerationEvents) -> Unit } +data class ModerationActionState( + val action: ModerationAction, + val isEnabled: Boolean, +) + sealed interface ModerationAction { - data class DisplayProfile(val user: MatrixUser) : ModerationAction - data class KickUser(val user: MatrixUser) : ModerationAction - data class BanUser(val user: MatrixUser) : ModerationAction - data class UnbanUser(val user: MatrixUser) : ModerationAction + data object DisplayProfile : ModerationAction + data object KickUser : ModerationAction + data object BanUser : ModerationAction + data object UnbanUser : ModerationAction } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt index 9a9cce3eb7..681a1eb733 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt @@ -15,6 +15,7 @@ import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.user.MatrixUser import timber.log.Timber import javax.inject.Inject @@ -23,7 +24,7 @@ class DefaultRoomMemberModerationRenderer @Inject constructor() : RoomMemberMode @Composable override fun Render( state: RoomMemberModerationState, - onSelectAction: (ModerationAction) -> Unit, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, modifier: Modifier ) { if (state is InternalRoomMemberModerationState) { diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt index 7e0c27a62d..b4bb8a3a27 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -8,6 +8,7 @@ package io.element.android.features.roommembermoderation.impl import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction @@ -20,7 +21,7 @@ data class InternalRoomMemberModerationState( override val canKick: Boolean, override val canBan: Boolean, val selectedUser: MatrixUser?, - val actions: ImmutableList, + val actions: ImmutableList, val kickUserAsyncAction: AsyncAction, val banUserAsyncAction: AsyncAction, val unbanUserAsyncAction: AsyncAction, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index fb9199ea54..0359a3120e 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction @@ -26,7 +27,8 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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 -import io.element.android.libraries.matrix.api.room.toMatrixUser +import io.element.android.libraries.matrix.api.room.RoomMembershipState +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.canBanAsState import io.element.android.libraries.matrix.ui.room.canKickAsState @@ -64,38 +66,37 @@ class RoomMemberModerationPresenter @Inject constructor( var selectedUser by remember { mutableStateOf(null) } - val moderationActions = remember { mutableStateOf(persistentListOf()) } + val moderationActions = remember { mutableStateOf(persistentListOf()) } fun handleEvent(event: RoomMemberModerationEvents) { when (event) { is RoomMemberModerationEvents.ShowActionsForUser -> { coroutineScope.launch { selectedUser = event.user - moderationActions.value = persistentListOf(ModerationAction.DisplayProfile(event.user)) - room.getUpdatedMember(event.user.userId) - .onSuccess { - moderationActions.value = computeModerationActions( - member = it, - canKick = canKick.value, - canBan = canBan.value, - currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, - ) - } + val member = room.membersStateFlow.value.roomMembers()?.firstOrNull { + it.userId == event.user.userId + } + moderationActions.value = computeModerationActions( + member = member, + canKick = canKick.value, + canBan = canBan.value, + currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, + ) } } is RoomMemberModerationEvents.ProcessAction -> { when (val action = event.action) { is ModerationAction.DisplayProfile -> Unit is ModerationAction.KickUser -> { - selectedUser = action.user + selectedUser = event.targetUser kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams } is ModerationAction.BanUser -> { - selectedUser = action.user + selectedUser = event.targetUser banUserAsyncAction.value = AsyncAction.ConfirmingNoParams } is ModerationAction.UnbanUser -> { - selectedUser = action.user + selectedUser = event.targetUser unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams } } @@ -112,18 +113,18 @@ class RoomMemberModerationPresenter @Inject constructor( } selectedUser = null } - is InternalRoomMemberModerationEvents.Reset -> { - selectedUser = null - kickUserAsyncAction.value = AsyncAction.Uninitialized - banUserAsyncAction.value = AsyncAction.Uninitialized - unbanUserAsyncAction.value = AsyncAction.Uninitialized - } is InternalRoomMemberModerationEvents.DoUnbanUser -> { selectedUser?.let { coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) } selectedUser = null } + is InternalRoomMemberModerationEvents.Reset -> { + selectedUser = null + kickUserAsyncAction.value = AsyncAction.Uninitialized + banUserAsyncAction.value = AsyncAction.Uninitialized + unbanUserAsyncAction.value = AsyncAction.Uninitialized + } } } @@ -140,20 +141,27 @@ class RoomMemberModerationPresenter @Inject constructor( } private fun computeModerationActions( - member: RoomMember, + member: RoomMember?, canKick: Boolean, canBan: Boolean, currentUserMemberPowerLevel: Long, - ): PersistentList { - val memberAsUser = member.toMatrixUser() + ): PersistentList { return buildList { - add(ModerationAction.DisplayProfile(memberAsUser)) - val canModerateThisUser = member.powerLevel < currentUserMemberPowerLevel && member.membership.isActive() - if (canKick && canModerateThisUser) { - add(ModerationAction.KickUser(memberAsUser)) + add(ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true)) + // Assume the member is a regular user when it's unknown + val canModerateThisUser = (member?.powerLevel ?: 0) < currentUserMemberPowerLevel + // Assume the member is joined when it's unknown + val membership = member?.membership ?: RoomMembershipState.JOIN + if (canKick) { + val isKickEnabled = canModerateThisUser && membership.isActive() + add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = isKickEnabled)) } - if (canBan && canModerateThisUser) { - add(ModerationAction.BanUser(memberAsUser)) + if (canBan) { + if (membership == RoomMembershipState.BAN) { + add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser)) + } else { + add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser)) + } } }.toPersistentList() } @@ -204,5 +212,4 @@ class RoomMemberModerationPresenter @Inject constructor( } } } - } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt index 9d52eec503..cdfca792d6 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt @@ -9,6 +9,7 @@ package io.element.android.features.roommembermoderation.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -25,24 +26,32 @@ class RoomMemberModerationStateProvider : PreviewParameterProvider = emptyList(), + actions: List = emptyList(), kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index 31b474057d..a2a86e4d13 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost @@ -58,7 +59,7 @@ import timber.log.Timber @Composable fun RoomMemberModerationView( state: InternalRoomMemberModerationState, - onSelectAction: (ModerationAction) -> Unit, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { @@ -201,8 +202,8 @@ private fun RoomMemberAsyncActions( @Composable private fun RoomMemberActionsBottomSheet( user: MatrixUser, - actions: ImmutableList, - onSelectAction: (ModerationAction) -> Unit, + actions: ImmutableList, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, onDismiss: () -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -223,8 +224,8 @@ private fun RoomMemberActionsBottomSheet( Avatar( avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser), modifier = Modifier - .padding(bottom = 28.dp) - .align(Alignment.CenterHorizontally) + .padding(bottom = 28.dp) + .align(Alignment.CenterHorizontally) ) user.displayName?.let { Text( @@ -234,8 +235,8 @@ private fun RoomMemberActionsBottomSheet( overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + .fillMaxWidth() ) } Text( @@ -246,35 +247,38 @@ private fun RoomMemberActionsBottomSheet( overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() + .padding(horizontal = 16.dp) + .fillMaxWidth() ) Spacer(modifier = Modifier.height(32.dp)) - for (action in actions) { - when (action) { + for (actionState in actions) { + when (val action = actionState.action) { is ModerationAction.DisplayProfile -> { ListItem( headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_member_user_info)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())), onClick = { coroutineScope.launch { - onSelectAction(action) + onSelectAction(action, user) bottomSheetState.hide() } - } + }, + enabled = actionState.isEnabled ) } is ModerationAction.KickUser -> { ListItem( headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_remove)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())), + style = ListItemStyle.Destructive, onClick = { coroutineScope.launch { bottomSheetState.hide() - onSelectAction(action) + onSelectAction(action, user) } - } + }, + enabled = actionState.isEnabled ) } is ModerationAction.BanUser -> { @@ -285,12 +289,26 @@ private fun RoomMemberActionsBottomSheet( onClick = { coroutineScope.launch { bottomSheetState.hide() - onSelectAction(action) + onSelectAction(action, user) } - } + }, + enabled = actionState.isEnabled + ) + } + is ModerationAction.UnbanUser -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_unban_action)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Restart())), + style = ListItemStyle.Destructive, + onClick = { + coroutineScope.launch { + bottomSheetState.hide() + onSelectAction(action, user) + } + }, + enabled = actionState.isEnabled ) } - is ModerationAction.UnbanUser -> Unit } } } @@ -303,12 +321,14 @@ internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMemberModera ElementPreview { Box( modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp) + .fillMaxWidth() + .heightIn(min = 64.dp) ) { RoomMemberModerationView( state = state, - onSelectAction = {}, + onSelectAction = { _, _ -> + + }, ) } }