Browse Source

Room moderation: make it more reactive and simplify the code.

pull/3671/head
Benoit Marty 1 week ago committed by Benoit Marty
parent
commit
598d86607a
  1. 15
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingBanUser.kt
  2. 3
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt
  3. 96
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt
  4. 2
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt
  5. 23
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt
  6. 51
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt
  7. 7
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt
  8. 33
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt

15
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingBanUser.kt

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

3
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt

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

96
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt

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

2
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt

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

23
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt

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

51
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt

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

7
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt

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

33
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt

@ -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…
Cancel
Save