change (member moderation) : branch moderation on timeline
This commit is contained in:
parent
58d9b12ab3
commit
a09cc8de97
22 changed files with 169 additions and 95 deletions
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue