Room moderation: make it more reactive and simplify the code.
This commit is contained in:
parent
ba9f0018f8
commit
d468a009d5
8 changed files with 143 additions and 87 deletions
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomdetails.impl.members.moderation
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
||||||
|
data class ConfirmingBanUser(
|
||||||
|
val roomMember: RoomMember,
|
||||||
|
) : AsyncAction.Confirming
|
||||||
|
|
@ -7,12 +7,13 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members.moderation
|
package io.element.android.features.roomdetails.impl.members.moderation
|
||||||
|
|
||||||
|
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.RoomMember
|
||||||
|
|
||||||
sealed interface RoomMembersModerationEvents {
|
sealed interface RoomMembersModerationEvents {
|
||||||
data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents
|
data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents
|
||||||
data object KickUser : RoomMembersModerationEvents
|
data object KickUser : RoomMembersModerationEvents
|
||||||
data object BanUser : RoomMembersModerationEvents
|
data object BanUser : RoomMembersModerationEvents
|
||||||
data object UnbanUser : RoomMembersModerationEvents
|
data class UnbanUser(val userId: UserId) : RoomMembersModerationEvents
|
||||||
data object Reset : RoomMembersModerationEvents
|
data object Reset : RoomMembersModerationEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ package io.element.android.features.roomdetails.impl.members.moderation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
|
@ -21,16 +21,15 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.extensions.finally
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
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.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
import io.element.android.libraries.matrix.ui.room.canBanAsState
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
|
import io.element.android.libraries.matrix.ui.room.canKickAsState
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canKick
|
import io.element.android.libraries.matrix.ui.room.isDmAsState
|
||||||
|
import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
|
|
@ -45,21 +44,39 @@ class RoomMembersModerationPresenter @Inject constructor(
|
||||||
) : Presenter<RoomMembersModerationState> {
|
) : Presenter<RoomMembersModerationState> {
|
||||||
private var selectedMember by mutableStateOf<RoomMember?>(null)
|
private var selectedMember by mutableStateOf<RoomMember?>(null)
|
||||||
|
|
||||||
private suspend fun canBan() = room.canBan().getOrDefault(false)
|
|
||||||
private suspend fun canKick() = room.canKick().getOrDefault(false)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomMembersModerationState {
|
override fun present(): RoomMembersModerationState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var moderationActions by remember { mutableStateOf(persistentListOf<ModerationAction>()) }
|
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
val canDisplayModerationActions by produceState(
|
val canBan by room.canBanAsState(syncUpdateFlow.value)
|
||||||
initialValue = false,
|
val canKick by room.canKickAsState(syncUpdateFlow.value)
|
||||||
key1 = syncUpdateFlow.value
|
val isDm by room.isDmAsState(syncUpdateFlow.value)
|
||||||
) {
|
val currentUserMemberPowerLevel by room.userPowerLevelAsState(syncUpdateFlow.value)
|
||||||
value = !room.isDm && (canBan() || canKick())
|
|
||||||
|
val canDisplayModerationActions by remember {
|
||||||
|
derivedStateOf { !isDm && (canBan || canKick) }
|
||||||
}
|
}
|
||||||
|
val canDisplayBannedUsers by remember {
|
||||||
|
derivedStateOf { !isDm && canBan }
|
||||||
|
}
|
||||||
|
val moderationActions by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
buildList {
|
||||||
|
selectedMember?.let { roomMember ->
|
||||||
|
add(ModerationAction.DisplayProfile(roomMember.userId))
|
||||||
|
if (currentUserMemberPowerLevel > roomMember.powerLevel) {
|
||||||
|
if (canKick) {
|
||||||
|
add(ModerationAction.KickUser(roomMember.userId))
|
||||||
|
}
|
||||||
|
if (canBan) {
|
||||||
|
add(ModerationAction.BanUser(roomMember.userId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toPersistentList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val kickUserAsyncAction =
|
val kickUserAsyncAction =
|
||||||
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||||
val banUserAsyncAction =
|
val banUserAsyncAction =
|
||||||
|
|
@ -67,64 +84,37 @@ class RoomMembersModerationPresenter @Inject constructor(
|
||||||
val unbanUserAsyncAction =
|
val unbanUserAsyncAction =
|
||||||
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||||
|
|
||||||
val canDisplayBannedUsers by produceState(initialValue = false) {
|
|
||||||
value = !room.isDm && canBan()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvent(event: RoomMembersModerationEvents) {
|
fun handleEvent(event: RoomMembersModerationEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is RoomMembersModerationEvents.SelectRoomMember -> {
|
is RoomMembersModerationEvents.SelectRoomMember -> {
|
||||||
coroutineScope.launch {
|
if (event.roomMember.membership == RoomMembershipState.BAN && canBan) {
|
||||||
|
unbanUserAsyncAction.value = ConfirmingBanUser(event.roomMember)
|
||||||
|
} else {
|
||||||
selectedMember = event.roomMember
|
selectedMember = event.roomMember
|
||||||
if (event.roomMember.membership == RoomMembershipState.BAN && canBan()) {
|
|
||||||
unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
|
||||||
} else {
|
|
||||||
moderationActions = buildList {
|
|
||||||
add(ModerationAction.DisplayProfile(event.roomMember.userId))
|
|
||||||
val currentUserMemberPowerLevel = room.userRole(room.sessionId)
|
|
||||||
.getOrDefault(RoomMember.Role.USER)
|
|
||||||
.powerLevel
|
|
||||||
if (currentUserMemberPowerLevel > event.roomMember.powerLevel) {
|
|
||||||
if (canKick()) {
|
|
||||||
add(ModerationAction.KickUser(event.roomMember.userId))
|
|
||||||
}
|
|
||||||
if (canBan()) {
|
|
||||||
add(ModerationAction.BanUser(event.roomMember.userId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toPersistentList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is RoomMembersModerationEvents.KickUser -> {
|
is RoomMembersModerationEvents.KickUser -> {
|
||||||
moderationActions = persistentListOf()
|
|
||||||
selectedMember?.let {
|
selectedMember?.let {
|
||||||
coroutineScope.kickUser(it.userId, kickUserAsyncAction)
|
coroutineScope.kickUser(it.userId, kickUserAsyncAction)
|
||||||
}
|
}
|
||||||
|
selectedMember = null
|
||||||
}
|
}
|
||||||
is RoomMembersModerationEvents.BanUser -> {
|
is RoomMembersModerationEvents.BanUser -> {
|
||||||
if (banUserAsyncAction.value.isConfirming()) {
|
if (banUserAsyncAction.value.isConfirming()) {
|
||||||
moderationActions = persistentListOf()
|
|
||||||
selectedMember?.let {
|
selectedMember?.let {
|
||||||
coroutineScope.banUser(it.userId, banUserAsyncAction)
|
coroutineScope.banUser(it.userId, banUserAsyncAction)
|
||||||
}
|
}
|
||||||
|
selectedMember = null
|
||||||
} else {
|
} else {
|
||||||
banUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
banUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is RoomMembersModerationEvents.UnbanUser -> {
|
is RoomMembersModerationEvents.UnbanUser -> {
|
||||||
if (unbanUserAsyncAction.value.isConfirming()) {
|
// We are already confirming when we are reaching this point
|
||||||
moderationActions = persistentListOf()
|
coroutineScope.unbanUser(event.userId, unbanUserAsyncAction)
|
||||||
selectedMember?.let {
|
|
||||||
coroutineScope.unbanUser(it.userId, unbanUserAsyncAction)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is RoomMembersModerationEvents.Reset -> {
|
is RoomMembersModerationEvents.Reset -> {
|
||||||
selectedMember = null
|
selectedMember = null
|
||||||
moderationActions = persistentListOf()
|
|
||||||
kickUserAsyncAction.value = AsyncAction.Uninitialized
|
kickUserAsyncAction.value = AsyncAction.Uninitialized
|
||||||
banUserAsyncAction.value = AsyncAction.Uninitialized
|
banUserAsyncAction.value = AsyncAction.Uninitialized
|
||||||
unbanUserAsyncAction.value = AsyncAction.Uninitialized
|
unbanUserAsyncAction.value = AsyncAction.Uninitialized
|
||||||
|
|
@ -149,7 +139,7 @@ class RoomMembersModerationPresenter @Inject constructor(
|
||||||
kickUserAction: MutableState<AsyncAction<Unit>>,
|
kickUserAction: MutableState<AsyncAction<Unit>>,
|
||||||
) = runActionAndWaitForMembershipChange(kickUserAction) {
|
) = runActionAndWaitForMembershipChange(kickUserAction) {
|
||||||
analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember))
|
analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember))
|
||||||
room.kickUser(userId).finally { selectedMember = null }
|
room.kickUser(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.banUser(
|
private fun CoroutineScope.banUser(
|
||||||
|
|
@ -157,7 +147,7 @@ class RoomMembersModerationPresenter @Inject constructor(
|
||||||
banUserAction: MutableState<AsyncAction<Unit>>,
|
banUserAction: MutableState<AsyncAction<Unit>>,
|
||||||
) = runActionAndWaitForMembershipChange(banUserAction) {
|
) = runActionAndWaitForMembershipChange(banUserAction) {
|
||||||
analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember))
|
analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember))
|
||||||
room.banUser(userId).finally { selectedMember = null }
|
room.banUser(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.unbanUser(
|
private fun CoroutineScope.unbanUser(
|
||||||
|
|
@ -165,7 +155,7 @@ class RoomMembersModerationPresenter @Inject constructor(
|
||||||
unbanUserAction: MutableState<AsyncAction<Unit>>,
|
unbanUserAction: MutableState<AsyncAction<Unit>>,
|
||||||
) = runActionAndWaitForMembershipChange(unbanUserAction) {
|
) = runActionAndWaitForMembershipChange(unbanUserAction) {
|
||||||
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
|
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
|
||||||
room.unbanUser(userId).finally { selectedMember = null }
|
room.unbanUser(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(
|
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class RoomMembersModerationStateProvider : PreviewParameterProvider<RoomMembersM
|
||||||
),
|
),
|
||||||
aRoomMembersModerationState(
|
aRoomMembersModerationState(
|
||||||
selectedRoomMember = anAlice(),
|
selectedRoomMember = anAlice(),
|
||||||
unbanUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
unbanUserAsyncAction = ConfirmingBanUser(anAlice()),
|
||||||
),
|
),
|
||||||
aRoomMembersModerationState(
|
aRoomMembersModerationState(
|
||||||
kickUserAsyncAction = AsyncAction.Success(Unit),
|
kickUserAsyncAction = AsyncAction.Success(Unit),
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ fun RoomMembersModerationView(
|
||||||
title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title),
|
title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title),
|
||||||
content = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description),
|
content = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description),
|
||||||
submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action),
|
submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action),
|
||||||
onSubmitClick = { state.selectedRoomMember?.userId?.let { state.eventSink(RoomMembersModerationEvents.BanUser) } },
|
onSubmitClick = { state.eventSink(RoomMembersModerationEvents.BanUser) },
|
||||||
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }
|
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -147,24 +147,22 @@ fun RoomMembersModerationView(
|
||||||
|
|
||||||
when (val action = state.unbanUserAsyncAction) {
|
when (val action = state.unbanUserAsyncAction) {
|
||||||
is AsyncAction.Confirming -> {
|
is AsyncAction.Confirming -> {
|
||||||
state.selectedRoomMember?.let {
|
if (action is ConfirmingBanUser) {
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
title = stringResource(R.string.screen_room_member_list_manage_member_unban_title),
|
title = stringResource(R.string.screen_room_member_list_manage_member_unban_title),
|
||||||
content = stringResource(R.string.screen_room_member_list_manage_member_unban_message),
|
content = stringResource(R.string.screen_room_member_list_manage_member_unban_message),
|
||||||
submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action),
|
submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action),
|
||||||
onSubmitClick = { state.eventSink(RoomMembersModerationEvents.UnbanUser) },
|
onSubmitClick = {
|
||||||
|
val userDisplayName = action.roomMember.getBestName()
|
||||||
|
asyncIndicatorState.enqueue {
|
||||||
|
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName))
|
||||||
|
}
|
||||||
|
state.eventSink(RoomMembersModerationEvents.UnbanUser(action.roomMember.userId))
|
||||||
|
},
|
||||||
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AsyncAction.Loading -> {
|
|
||||||
LaunchedEffect(action) {
|
|
||||||
val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty()
|
|
||||||
asyncIndicatorState.enqueue {
|
|
||||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is AsyncAction.Failure -> {
|
is AsyncAction.Failure -> {
|
||||||
Timber.e(action.error, "Failed to unban user.")
|
Timber.e(action.error, "Failed to unban user.")
|
||||||
LaunchedEffect(action) {
|
LaunchedEffect(action) {
|
||||||
|
|
@ -178,7 +176,8 @@ fun RoomMembersModerationView(
|
||||||
is AsyncAction.Success -> {
|
is AsyncAction.Success -> {
|
||||||
LaunchedEffect(action) { asyncIndicatorState.clear() }
|
LaunchedEffect(action) { asyncIndicatorState.clear() }
|
||||||
}
|
}
|
||||||
else -> Unit
|
is AsyncAction.Loading,
|
||||||
|
AsyncAction.Uninitialized -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.RoomModeration
|
import im.vector.app.features.analytics.plan.RoomModeration
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.roomdetails.impl.members.aVictor
|
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||||
|
import io.element.android.features.roomdetails.impl.members.moderation.ConfirmingBanUser
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction
|
import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
|
||||||
|
|
@ -37,7 +38,14 @@ import org.junit.Test
|
||||||
class RoomMembersModerationPresenterTest {
|
class RoomMembersModerationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `canDisplayModerationActions - when room is DM is false`() = runTest {
|
fun `canDisplayModerationActions - when room is DM is false`() = runTest {
|
||||||
val room = FakeMatrixRoom(isDirect = true, isPublic = true, activeMemberCount = 2).apply {
|
val room = FakeMatrixRoom(
|
||||||
|
isDirect = true,
|
||||||
|
isPublic = true,
|
||||||
|
activeMemberCount = 2,
|
||||||
|
canKickResult = { Result.success(true) },
|
||||||
|
canBanResult = { Result.success(true) },
|
||||||
|
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
||||||
|
).apply {
|
||||||
givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2))
|
givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2))
|
||||||
}
|
}
|
||||||
val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
|
val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
|
||||||
|
|
@ -53,6 +61,7 @@ class RoomMembersModerationPresenterTest {
|
||||||
activeMemberCount = 10,
|
activeMemberCount = 10,
|
||||||
canKickResult = { Result.success(true) },
|
canKickResult = { Result.success(true) },
|
||||||
canBanResult = { Result.success(true) },
|
canBanResult = { Result.success(true) },
|
||||||
|
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
||||||
)
|
)
|
||||||
val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
|
val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -66,7 +75,9 @@ class RoomMembersModerationPresenterTest {
|
||||||
val room = FakeMatrixRoom(
|
val room = FakeMatrixRoom(
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
activeMemberCount = 10,
|
activeMemberCount = 10,
|
||||||
|
canKickResult = { Result.success(true) },
|
||||||
canBanResult = { Result.success(true) },
|
canBanResult = { Result.success(true) },
|
||||||
|
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
||||||
)
|
)
|
||||||
val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
|
val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -141,8 +152,8 @@ class RoomMembersModerationPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
assertThat(selectedRoomMember).isNotNull()
|
assertThat(selectedRoomMember).isNull()
|
||||||
assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
assertThat(unbanUserAsyncAction).isEqualTo(ConfirmingBanUser(selectedMember))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,8 +176,9 @@ class RoomMembersModerationPresenterTest {
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
|
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
assertThat(awaitItem().actions).isEmpty()
|
val loadingState = awaitItem()
|
||||||
assertThat(awaitItem().kickUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
assertThat(loadingState.actions).isEmpty()
|
||||||
|
assertThat(loadingState.kickUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
assertThat(selectedRoomMember).isNull()
|
assertThat(selectedRoomMember).isNull()
|
||||||
|
|
@ -198,8 +210,10 @@ class RoomMembersModerationPresenterTest {
|
||||||
// Confirm
|
// Confirm
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.BanUser)
|
confirmingState.eventSink(RoomMembersModerationEvents.BanUser)
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
assertThat(awaitItem().actions).isEmpty()
|
val loadingItem = awaitItem()
|
||||||
assertThat(awaitItem().banUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
assertThat(loadingItem.actions).isEmpty()
|
||||||
|
assertThat(loadingItem.selectedRoomMember).isNull()
|
||||||
|
assertThat(loadingItem.banUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
assertThat(selectedRoomMember).isNull()
|
assertThat(selectedRoomMember).isNull()
|
||||||
|
|
@ -225,11 +239,14 @@ class RoomMembersModerationPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
// Displays confirmation dialog
|
// Displays unban confirmation dialog
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||||
|
val confirmingState = awaitItem()
|
||||||
|
assertThat(confirmingState.selectedRoomMember).isNull()
|
||||||
|
assertThat(confirmingState.actions).isEmpty()
|
||||||
|
assertThat(confirmingState.unbanUserAsyncAction).isEqualTo(ConfirmingBanUser(selectedMember))
|
||||||
// Confirms unban
|
// Confirms unban
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.UnbanUser)
|
confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(selectedMember.userId))
|
||||||
assertThat(awaitItem().actions).isEmpty()
|
|
||||||
assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
|
@ -251,12 +268,13 @@ class RoomMembersModerationPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
// Displays confirmation dialog
|
// Select a user
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
||||||
// Reset state
|
// Reset state
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.Reset)
|
awaitItem().eventSink(RoomMembersModerationEvents.Reset)
|
||||||
assertThat(awaitItem().selectedRoomMember).isNull()
|
val finalItem = awaitItem()
|
||||||
assertThat(awaitItem().actions).isEmpty()
|
assertThat(finalItem.selectedRoomMember).isNull()
|
||||||
|
assertThat(finalItem.actions).isEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,7 +296,7 @@ class RoomMembersModerationPresenterTest {
|
||||||
// Kick user and fail
|
// Kick user and fail
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
|
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
|
||||||
skipItems(2)
|
skipItems(1)
|
||||||
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
// Reset it
|
// Reset it
|
||||||
|
|
@ -289,7 +307,7 @@ class RoomMembersModerationPresenterTest {
|
||||||
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
||||||
skipItems(2)
|
skipItems(1)
|
||||||
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
// Reset it
|
// Reset it
|
||||||
|
|
@ -300,8 +318,7 @@ class RoomMembersModerationPresenterTest {
|
||||||
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor().copy(membership = RoomMembershipState.BAN)))
|
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor().copy(membership = RoomMembershipState.BAN)))
|
||||||
val confirmingState = awaitItem()
|
val confirmingState = awaitItem()
|
||||||
assertThat(confirmingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
assertThat(confirmingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser)
|
confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(aVictor().userId))
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
// Reset it
|
// Reset it
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.features.roomdetails.impl.members.anAlice
|
import io.element.android.features.roomdetails.impl.members.anAlice
|
||||||
|
import io.element.android.features.roomdetails.impl.members.moderation.ConfirmingBanUser
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction
|
import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||||
|
|
@ -164,7 +165,7 @@ class RoomMembersModerationViewTest {
|
||||||
val roomMember = anAlice()
|
val roomMember = anAlice()
|
||||||
val state = aRoomMembersModerationState(
|
val state = aRoomMembersModerationState(
|
||||||
selectedRoomMember = roomMember,
|
selectedRoomMember = roomMember,
|
||||||
unbanUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
unbanUserAsyncAction = ConfirmingBanUser(roomMember),
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
rule.setRoomMembersModerationView(
|
rule.setRoomMembersModerationView(
|
||||||
|
|
@ -181,7 +182,7 @@ class RoomMembersModerationViewTest {
|
||||||
val roomMember = anAlice()
|
val roomMember = anAlice()
|
||||||
val state = aRoomMembersModerationState(
|
val state = aRoomMembersModerationState(
|
||||||
selectedRoomMember = roomMember,
|
selectedRoomMember = roomMember,
|
||||||
unbanUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
unbanUserAsyncAction = ConfirmingBanUser(roomMember),
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
rule.setRoomMembersModerationView(
|
rule.setRoomMembersModerationView(
|
||||||
|
|
@ -189,7 +190,7 @@ class RoomMembersModerationViewTest {
|
||||||
)
|
)
|
||||||
// Note: the string key semantics is not perfect here :/
|
// Note: the string key semantics is not perfect here :/
|
||||||
rule.clickOn(R.string.screen_room_member_list_manage_member_unban_action)
|
rule.clickOn(R.string.screen_room_member_list_manage_member_unban_action)
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.UnbanUser)
|
eventsRecorder.assertSingle(RoomMembersModerationEvents.UnbanUser(roomMember.userId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@ import androidx.compose.runtime.produceState
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
|
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
||||||
|
import io.element.android.libraries.matrix.api.room.powerlevels.canKick
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
||||||
|
|
@ -62,6 +65,36 @@ fun MatrixRoom.canPinUnpin(updateKey: Long): State<Boolean> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MatrixRoom.isDmAsState(updateKey: Long): State<Boolean> {
|
||||||
|
return produceState(initialValue = false, key1 = updateKey) {
|
||||||
|
value = isDm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MatrixRoom.canKickAsState(updateKey: Long): State<Boolean> {
|
||||||
|
return produceState(initialValue = false, key1 = updateKey) {
|
||||||
|
value = canKick().getOrElse { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MatrixRoom.canBanAsState(updateKey: Long): State<Boolean> {
|
||||||
|
return produceState(initialValue = false, key1 = updateKey) {
|
||||||
|
value = canBan().getOrElse { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
|
||||||
|
return produceState(initialValue = 0, key1 = updateKey) {
|
||||||
|
value = userRole(sessionId)
|
||||||
|
.getOrDefault(RoomMember.Role.USER)
|
||||||
|
.powerLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MatrixRoom.isOwnUserAdmin(): Boolean {
|
fun MatrixRoom.isOwnUserAdmin(): Boolean {
|
||||||
val roomInfo by roomInfoFlow.collectAsState(initial = null)
|
val roomInfo by roomInfoFlow.collectAsState(initial = null)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue