change (member moderation) : allow disabled action and render unban too

This commit is contained in:
ganfra
2025-05-16 18:38:15 +02:00
parent f22b921768
commit c6eea91d69
11 changed files with 119 additions and 77 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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<ModerationAction>,
val actions: ImmutableList<ModerationActionState>,
val kickUserAsyncAction: AsyncAction<Unit>,
val banUserAsyncAction: AsyncAction<Unit>,
val unbanUserAsyncAction: AsyncAction<Unit>,

View File

@@ -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<MatrixUser?>(null)
}
val moderationActions = remember { mutableStateOf(persistentListOf<ModerationAction>()) }
val moderationActions = remember { mutableStateOf(persistentListOf<ModerationActionState>()) }
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<ModerationAction> {
val memberAsUser = member.toMatrixUser()
): PersistentList<ModerationActionState> {
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(
}
}
}
}

View File

@@ -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<InternalRoomM
aRoomMembersModerationState(
selectedUser = anAlice(),
actions = listOf(
ModerationAction.DisplayProfile(anAlice()),
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
),
),
aRoomMembersModerationState(
selectedUser = anAlice(),
actions = listOf(
ModerationAction.DisplayProfile(anAlice()),
ModerationAction.KickUser(anAlice()),
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
ModerationActionState(action = ModerationAction.KickUser, isEnabled = true),
),
),
aRoomMembersModerationState(
selectedUser = anAlice(),
actions = listOf(
ModerationAction.DisplayProfile(anAlice()),
ModerationAction.KickUser(anAlice()),
ModerationAction.BanUser(anAlice()),
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
ModerationActionState(action = ModerationAction.KickUser, isEnabled = false),
ModerationActionState(action = ModerationAction.BanUser, isEnabled = true),
),
),
aRoomMembersModerationState(
selectedUser = anAlice(),
actions = listOf(
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
ModerationActionState(action = ModerationAction.KickUser, isEnabled = false),
ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = true),
),
),
aRoomMembersModerationState(
selectedUser = anAlice(),
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
@@ -72,7 +81,7 @@ fun aRoomMembersModerationState(
canKick: Boolean = false,
canBan: Boolean = false,
selectedUser: MatrixUser? = null,
actions: List<ModerationAction> = emptyList(),
actions: List<ModerationActionState> = emptyList(),
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
banUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
unbanUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,

View File

@@ -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<ModerationAction>,
onSelectAction: (ModerationAction) -> Unit,
actions: ImmutableList<ModerationActionState>,
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 = { _, _ ->
},
)
}
}