change (member moderation) : branch moderation on timeline

This commit is contained in:
ganfra 2025-05-13 11:39:19 +02:00
parent 58d9b12ab3
commit a09cc8de97
22 changed files with 169 additions and 95 deletions

View file

@ -13,20 +13,19 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class InternalRoomMemberModerationState(
override val canKick: Boolean,
override val canBan: Boolean,
val selectedRoomMember: AsyncData<RoomMember>,
val selectedUser: MatrixUser?,
val actions: ImmutableList<ModerationAction>,
val kickUserAsyncAction: AsyncAction<Unit>,
val banUserAsyncAction: AsyncAction<Unit>,
val unbanUserAsyncAction: AsyncAction<Unit>,
override val eventSink: (RoomMemberModerationEvents) -> Unit,
) : RoomMemberModerationState {
val canOnlyDisplayProfile = actions.size == 1 && actions.first() is ModerationAction.DisplayProfile
val canDisplayActions = actions.isNotEmpty() && !canOnlyDisplayProfile
val canDisplayActions = actions.isNotEmpty()
}

View file

@ -20,13 +20,14 @@ import io.element.android.features.roommembermoderation.api.ModerationAction
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
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.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.user.MatrixUser
import io.element.android.libraries.matrix.ui.room.canBanAsState
import io.element.android.libraries.matrix.ui.room.canKickAsState
import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState
@ -45,7 +46,6 @@ class RoomMemberModerationPresenter @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val analyticsService: AnalyticsService,
) : Presenter<RoomMemberModerationState> {
private var selectedMember by mutableStateOf<AsyncData<RoomMember>>(AsyncData.Uninitialized)
@Composable
override fun present(): RoomMemberModerationState {
@ -61,60 +61,68 @@ class RoomMemberModerationPresenter @Inject constructor(
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val unbanUserAsyncAction =
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
var selectedUser by remember {
mutableStateOf<MatrixUser?>(null)
}
val moderationActions = remember { mutableStateOf(persistentListOf<ModerationAction>()) }
fun handleEvent(event: RoomMemberModerationEvents) {
when (event) {
is RoomMemberModerationEvents.RenderActions -> {
selectedMember = AsyncData.Success(event.roomMember)
moderationActions.value = computeModerationActions(
member = event.roomMember,
canKick = canKick.value,
canBan = canBan.value,
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
)
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,
)
}
}
}
is RoomMemberModerationEvents.ProcessAction -> {
when(val action = event.action) {
when (val action = event.action) {
is ModerationAction.DisplayProfile -> Unit
is ModerationAction.KickUser -> {
selectedMember = AsyncData.Success(action.member)
selectedUser = action.user
kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams
}
is ModerationAction.BanUser -> {
selectedMember = AsyncData.Success(action.member)
selectedUser = action.user
banUserAsyncAction.value = AsyncAction.ConfirmingNoParams
}
is ModerationAction.UnbanUser -> {
selectedMember = AsyncData.Success(action.member)
selectedUser = action.user
unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams
}
}
}
is InternalRoomMemberModerationEvents.DoKickUser -> {
selectedMember.dataOrNull()?.let {
selectedUser?.let {
coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction)
}
selectedMember = AsyncData.Uninitialized
selectedUser = null
}
is InternalRoomMemberModerationEvents.DoBanUser -> {
selectedMember.dataOrNull()?.let {
selectedUser?.let {
coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction)
}
selectedMember = AsyncData.Uninitialized
selectedUser = null
}
is InternalRoomMemberModerationEvents.Reset -> {
selectedMember = AsyncData.Uninitialized
selectedUser = null
kickUserAsyncAction.value = AsyncAction.Uninitialized
banUserAsyncAction.value = AsyncAction.Uninitialized
unbanUserAsyncAction.value = AsyncAction.Uninitialized
}
is InternalRoomMemberModerationEvents.DoUnbanUser -> {
selectedMember.dataOrNull()?.let {
selectedUser?.let {
coroutineScope.unbanUser(it.userId, unbanUserAsyncAction)
}
selectedMember = AsyncData.Uninitialized
selectedUser = null
}
}
}
@ -122,7 +130,7 @@ class RoomMemberModerationPresenter @Inject constructor(
return InternalRoomMemberModerationState(
canKick = canKick.value,
canBan = canBan.value,
selectedRoomMember = selectedMember,
selectedUser = selectedUser,
actions = moderationActions.value,
kickUserAsyncAction = kickUserAsyncAction.value,
banUserAsyncAction = banUserAsyncAction.value,
@ -137,13 +145,15 @@ class RoomMemberModerationPresenter @Inject constructor(
canBan: Boolean,
currentUserMemberPowerLevel: Long,
): PersistentList<ModerationAction> {
val memberAsUser = member.toMatrixUser()
return buildList {
add(ModerationAction.DisplayProfile(member))
if (canKick && member.powerLevel < currentUserMemberPowerLevel) {
add(ModerationAction.KickUser(member))
add(ModerationAction.DisplayProfile(memberAsUser))
val canModerateThisUser = member.powerLevel < currentUserMemberPowerLevel && member.membership.isActive()
if (canKick && canModerateThisUser) {
add(ModerationAction.KickUser(memberAsUser))
}
if (canBan && member.powerLevel < currentUserMemberPowerLevel) {
add(ModerationAction.BanUser(member))
if (canBan && canModerateThisUser) {
add(ModerationAction.BanUser(memberAsUser))
}
}.toPersistentList()
}
@ -194,4 +204,5 @@ class RoomMemberModerationPresenter @Inject constructor(
}
}
}
}

View file

@ -15,26 +15,28 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.toMatrixUser
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.toPersistentList
class RoomMemberModerationStateProvider : PreviewParameterProvider<InternalRoomMemberModerationState> {
override val values: Sequence<InternalRoomMemberModerationState>
get() = sequenceOf(
aRoomMembersModerationState(
selectedRoomMember = AsyncData.Success(anAlice()),
selectedUser = anAlice(),
actions = listOf(
ModerationAction.DisplayProfile(anAlice()),
),
),
aRoomMembersModerationState(
selectedRoomMember = AsyncData.Success(anAlice()),
selectedUser = anAlice(),
actions = listOf(
ModerationAction.DisplayProfile(anAlice()),
ModerationAction.KickUser(anAlice()),
),
),
aRoomMembersModerationState(
selectedRoomMember = AsyncData.Success(anAlice()),
selectedUser = anAlice(),
actions = listOf(
ModerationAction.DisplayProfile(anAlice()),
ModerationAction.KickUser(anAlice()),
@ -42,41 +44,34 @@ class RoomMemberModerationStateProvider : PreviewParameterProvider<InternalRoomM
),
),
aRoomMembersModerationState(
selectedRoomMember = AsyncData.Success(anAlice()),
selectedUser = anAlice(),
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
),
aRoomMembersModerationState(
selectedRoomMember = AsyncData.Success(anAlice()),
selectedUser = anAlice(),
kickUserAsyncAction = AsyncAction.Loading,
),
aRoomMembersModerationState(
selectedRoomMember = AsyncData.Success(anAlice()),
selectedUser = anAlice(),
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
),
aRoomMembersModerationState(
selectedRoomMember = AsyncData.Success(anAlice()),
selectedUser = anAlice(),
banUserAsyncAction = AsyncAction.Loading,
),
)
}
fun anAlice() = RoomMember(
fun anAlice() = MatrixUser(
UserId(value = "@alice:server.org"),
displayName = "Alice",
avatarUrl = null,
role = RoomMember.Role.forPowerLevel(100L),
membership = RoomMembershipState.JOIN,
isNameAmbiguous = false,
powerLevel = 100L,
normalizedPowerLevel = 100L,
isIgnored = false,
membershipChangeReason = null,
)
fun aRoomMembersModerationState(
canKick: Boolean = false,
canBan: Boolean = false,
selectedRoomMember: AsyncData<RoomMember> = AsyncData.Uninitialized,
selectedUser: MatrixUser? = null,
actions: List<ModerationAction> = emptyList(),
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
banUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
@ -85,7 +80,7 @@ fun aRoomMembersModerationState(
) = InternalRoomMemberModerationState(
canKick = canKick,
canBan = canBan,
selectedRoomMember = selectedRoomMember,
selectedUser = selectedUser,
actions = actions.toPersistentList(),
kickUserAsyncAction = kickUserAsyncAction,
banUserAsyncAction = banUserAsyncAction,

View file

@ -20,9 +20,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -49,9 +47,9 @@ 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.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch
@ -63,23 +61,27 @@ fun RoomMemberModerationView(
onSelectAction: (ModerationAction) -> Unit,
modifier: Modifier = Modifier,
) {
val selectedRoomMember = state.selectedRoomMember.dataOrNull()
Box(modifier = modifier) {
if (selectedRoomMember != null && state.canDisplayActions) {
val selectedUser = state.selectedUser
if (selectedUser != null && state.canDisplayActions) {
RoomMemberActionsBottomSheet(
roomMember = selectedRoomMember,
user = selectedUser,
actions = state.actions,
onSelectAction = onSelectAction,
onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) },
)
}
val onSelectAction by rememberUpdatedState(onSelectAction)
LaunchedEffect(state.canOnlyDisplayProfile) {
if (state.canOnlyDisplayProfile) {
onSelectAction(state.actions.first())
}
}
RoomMemberAsyncActions(state = state)
}
}
@Composable
private fun RoomMemberAsyncActions(
state: InternalRoomMemberModerationState,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
val selectedUser = state.selectedUser
val asyncIndicatorState = rememberAsyncIndicatorState()
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState)
@ -100,7 +102,7 @@ fun RoomMemberModerationView(
}
is AsyncAction.Loading -> {
LaunchedEffect(action) {
val userDisplayName = selectedRoomMember?.getBestName().orEmpty()
val userDisplayName = selectedUser?.getBestName().orEmpty()
asyncIndicatorState.enqueue {
AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_removing_user, userDisplayName))
}
@ -139,7 +141,7 @@ fun RoomMemberModerationView(
}
is AsyncAction.Loading -> {
LaunchedEffect(action) {
val userDisplayName = selectedRoomMember?.getBestName().orEmpty()
val userDisplayName = selectedUser?.getBestName().orEmpty()
asyncIndicatorState.enqueue {
AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_banning_user, userDisplayName))
}
@ -167,7 +169,7 @@ fun RoomMemberModerationView(
content = stringResource(R.string.screen_room_member_list_manage_member_unban_message),
submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action),
onSubmitClick = {
val userDisplayName = selectedRoomMember?.getBestName().orEmpty()
val userDisplayName = selectedUser?.getBestName().orEmpty()
asyncIndicatorState.enqueue {
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName))
}
@ -198,7 +200,7 @@ fun RoomMemberModerationView(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomMemberActionsBottomSheet(
roomMember: RoomMember,
user: MatrixUser,
actions: ImmutableList<ModerationAction>,
onSelectAction: (ModerationAction) -> Unit,
onDismiss: () -> Unit,
@ -219,12 +221,12 @@ private fun RoomMemberActionsBottomSheet(
modifier = Modifier.padding(vertical = 16.dp)
) {
Avatar(
avatarData = roomMember.getAvatarData(size = AvatarSize.RoomListManageUser),
avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser),
modifier = Modifier
.padding(bottom = 28.dp)
.align(Alignment.CenterHorizontally)
)
roomMember.displayName?.let {
user.displayName?.let {
Text(
text = it,
style = ElementTheme.typography.fontHeadingLgBold,
@ -237,7 +239,7 @@ private fun RoomMemberActionsBottomSheet(
)
}
Text(
text = roomMember.userId.toString(),
text = user.userId.toString(),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
maxLines = 1,