From 598d86607a24b1d3b890060cd09622ee6838d99c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 10 Oct 2024 13:44:24 +0200 Subject: [PATCH 1/3] Room moderation: make it more reactive and simplify the code. --- .../members/moderation/ConfirmingBanUser.kt | 15 +++ .../moderation/RoomMembersModerationEvents.kt | 3 +- .../RoomMembersModerationPresenter.kt | 96 +++++++++---------- .../RoomMembersModerationStateProvider.kt | 2 +- .../moderation/RoomMembersModerationView.kt | 23 +++-- .../RoomMembersModerationPresenterTest.kt | 51 ++++++---- .../RoomMembersModerationViewTest.kt | 7 +- .../matrix/ui/room/MatrixRoomState.kt | 33 +++++++ 8 files changed, 143 insertions(+), 87 deletions(-) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingBanUser.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingBanUser.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingBanUser.kt new file mode 100644 index 0000000000..fbfa60b5cc --- /dev/null +++ b/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 diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt index 02700264f2..15304207f2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt +++ b/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 +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember sealed interface RoomMembersModerationEvents { data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents data object KickUser : RoomMembersModerationEvents data object BanUser : RoomMembersModerationEvents - data object UnbanUser : RoomMembersModerationEvents + data class UnbanUser(val userId: UserId) : RoomMembersModerationEvents data object Reset : RoomMembersModerationEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt index 97e518fb47..c025fa1bbc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt +++ b/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.MutableState import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.runUpdatingState 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.room.MatrixRoom 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.isDm -import io.element.android.libraries.matrix.api.room.powerlevels.canBan -import io.element.android.libraries.matrix.api.room.powerlevels.canKick +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.isDmAsState +import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState import io.element.android.services.analytics.api.AnalyticsService -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.drop @@ -45,21 +44,39 @@ class RoomMembersModerationPresenter @Inject constructor( ) : Presenter { private var selectedMember by mutableStateOf(null) - private suspend fun canBan() = room.canBan().getOrDefault(false) - private suspend fun canKick() = room.canKick().getOrDefault(false) - @Composable override fun present(): RoomMembersModerationState { val coroutineScope = rememberCoroutineScope() - var moderationActions by remember { mutableStateOf(persistentListOf()) } - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val canDisplayModerationActions by produceState( - initialValue = false, - key1 = syncUpdateFlow.value - ) { - value = !room.isDm && (canBan() || canKick()) + val canBan by room.canBanAsState(syncUpdateFlow.value) + val canKick by room.canKickAsState(syncUpdateFlow.value) + val isDm by room.isDmAsState(syncUpdateFlow.value) + val currentUserMemberPowerLevel by room.userPowerLevelAsState(syncUpdateFlow.value) + + 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 = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } val banUserAsyncAction = @@ -67,64 +84,37 @@ class RoomMembersModerationPresenter @Inject constructor( val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - val canDisplayBannedUsers by produceState(initialValue = false) { - value = !room.isDm && canBan() - } - fun handleEvent(event: RoomMembersModerationEvents) { when (event) { is RoomMembersModerationEvents.SelectRoomMember -> { - coroutineScope.launch { + if (event.roomMember.membership == RoomMembershipState.BAN && canBan) { + unbanUserAsyncAction.value = ConfirmingBanUser(event.roomMember) + } else { 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 -> { - moderationActions = persistentListOf() selectedMember?.let { coroutineScope.kickUser(it.userId, kickUserAsyncAction) } + selectedMember = null } is RoomMembersModerationEvents.BanUser -> { if (banUserAsyncAction.value.isConfirming()) { - moderationActions = persistentListOf() selectedMember?.let { coroutineScope.banUser(it.userId, banUserAsyncAction) } + selectedMember = null } else { banUserAsyncAction.value = AsyncAction.ConfirmingNoParams } } is RoomMembersModerationEvents.UnbanUser -> { - if (unbanUserAsyncAction.value.isConfirming()) { - moderationActions = persistentListOf() - selectedMember?.let { - coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) - } - } else { - unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams - } + // We are already confirming when we are reaching this point + coroutineScope.unbanUser(event.userId, unbanUserAsyncAction) } is RoomMembersModerationEvents.Reset -> { selectedMember = null - moderationActions = persistentListOf() kickUserAsyncAction.value = AsyncAction.Uninitialized banUserAsyncAction.value = AsyncAction.Uninitialized unbanUserAsyncAction.value = AsyncAction.Uninitialized @@ -149,7 +139,7 @@ class RoomMembersModerationPresenter @Inject constructor( kickUserAction: MutableState>, ) = runActionAndWaitForMembershipChange(kickUserAction) { analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember)) - room.kickUser(userId).finally { selectedMember = null } + room.kickUser(userId) } private fun CoroutineScope.banUser( @@ -157,7 +147,7 @@ class RoomMembersModerationPresenter @Inject constructor( banUserAction: MutableState>, ) = runActionAndWaitForMembershipChange(banUserAction) { analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember)) - room.banUser(userId).finally { selectedMember = null } + room.banUser(userId) } private fun CoroutineScope.unbanUser( @@ -165,7 +155,7 @@ class RoomMembersModerationPresenter @Inject constructor( unbanUserAction: MutableState>, ) = runActionAndWaitForMembershipChange(unbanUserAction) { analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) - room.unbanUser(userId).finally { selectedMember = null } + room.unbanUser(userId) } private fun CoroutineScope.runActionAndWaitForMembershipChange( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt index 221bce15ff..2c2fb6d27c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt @@ -60,7 +60,7 @@ class RoomMembersModerationStateProvider : PreviewParameterProvider { - state.selectedRoomMember?.let { + if (action is ConfirmingBanUser) { ConfirmationDialog( title = stringResource(R.string.screen_room_member_list_manage_member_unban_title), 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 = { 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) }, ) } } - 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 -> { Timber.e(action.error, "Failed to unban user.") LaunchedEffect(action) { @@ -178,7 +176,8 @@ fun RoomMembersModerationView( is AsyncAction.Success -> { LaunchedEffect(action) { asyncIndicatorState.clear() } } - else -> Unit + is AsyncAction.Loading, + AsyncAction.Uninitialized -> Unit } } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt index a4308e23aa..1480f80716 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt +++ b/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 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.moderation.ConfirmingBanUser 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.RoomMembersModerationPresenter @@ -37,7 +38,14 @@ import org.junit.Test class RoomMembersModerationPresenterTest { @Test 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)) } val presenter = createRoomMembersModerationPresenter(matrixRoom = room) @@ -53,6 +61,7 @@ class RoomMembersModerationPresenterTest { activeMemberCount = 10, canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, + userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, ) val presenter = createRoomMembersModerationPresenter(matrixRoom = room) presenter.test { @@ -66,7 +75,9 @@ class RoomMembersModerationPresenterTest { val room = FakeMatrixRoom( isDirect = false, activeMemberCount = 10, + canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, + userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, ) val presenter = createRoomMembersModerationPresenter(matrixRoom = room) presenter.test { @@ -141,8 +152,8 @@ class RoomMembersModerationPresenterTest { skipItems(1) awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) with(awaitItem()) { - assertThat(selectedRoomMember).isNotNull() - assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) + assertThat(selectedRoomMember).isNull() + assertThat(unbanUserAsyncAction).isEqualTo(ConfirmingBanUser(selectedMember)) } } } @@ -165,8 +176,9 @@ class RoomMembersModerationPresenterTest { awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) awaitItem().eventSink(RoomMembersModerationEvents.KickUser) skipItems(1) - assertThat(awaitItem().actions).isEmpty() - assertThat(awaitItem().kickUserAsyncAction).isEqualTo(AsyncAction.Loading) + val loadingState = awaitItem() + assertThat(loadingState.actions).isEmpty() + assertThat(loadingState.kickUserAsyncAction).isEqualTo(AsyncAction.Loading) with(awaitItem()) { assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) assertThat(selectedRoomMember).isNull() @@ -198,8 +210,10 @@ class RoomMembersModerationPresenterTest { // Confirm confirmingState.eventSink(RoomMembersModerationEvents.BanUser) skipItems(1) - assertThat(awaitItem().actions).isEmpty() - assertThat(awaitItem().banUserAsyncAction).isEqualTo(AsyncAction.Loading) + val loadingItem = awaitItem() + assertThat(loadingItem.actions).isEmpty() + assertThat(loadingItem.selectedRoomMember).isNull() + assertThat(loadingItem.banUserAsyncAction).isEqualTo(AsyncAction.Loading) with(awaitItem()) { assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) assertThat(selectedRoomMember).isNull() @@ -225,11 +239,14 @@ class RoomMembersModerationPresenterTest { presenter.present() }.test { skipItems(1) - // Displays confirmation dialog + // Displays unban confirmation dialog awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) + val confirmingState = awaitItem() + assertThat(confirmingState.selectedRoomMember).isNull() + assertThat(confirmingState.actions).isEmpty() + assertThat(confirmingState.unbanUserAsyncAction).isEqualTo(ConfirmingBanUser(selectedMember)) // Confirms unban - awaitItem().eventSink(RoomMembersModerationEvents.UnbanUser) - assertThat(awaitItem().actions).isEmpty() + confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(selectedMember.userId)) assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading) with(awaitItem()) { assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) @@ -251,12 +268,13 @@ class RoomMembersModerationPresenterTest { presenter.present() }.test { skipItems(1) - // Displays confirmation dialog + // Select a user awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) // Reset state awaitItem().eventSink(RoomMembersModerationEvents.Reset) - assertThat(awaitItem().selectedRoomMember).isNull() - assertThat(awaitItem().actions).isEmpty() + val finalItem = awaitItem() + assertThat(finalItem.selectedRoomMember).isNull() + assertThat(finalItem.actions).isEmpty() } } @@ -278,7 +296,7 @@ class RoomMembersModerationPresenterTest { // Kick user and fail awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) awaitItem().eventSink(RoomMembersModerationEvents.KickUser) - skipItems(2) + skipItems(1) assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) // Reset it @@ -289,7 +307,7 @@ class RoomMembersModerationPresenterTest { initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) 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.Failure::class.java) // Reset it @@ -300,8 +318,7 @@ class RoomMembersModerationPresenterTest { initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor().copy(membership = RoomMembershipState.BAN))) val confirmingState = awaitItem() assertThat(confirmingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Confirming::class.java) - confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser) - skipItems(1) + confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(aVictor().userId)) assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) // Reset it diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt index 723f94d20a..41821411f9 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt +++ b/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 io.element.android.features.roomdetails.impl.R 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.RoomMembersModerationEvents import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState @@ -164,7 +165,7 @@ class RoomMembersModerationViewTest { val roomMember = anAlice() val state = aRoomMembersModerationState( selectedRoomMember = roomMember, - unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, + unbanUserAsyncAction = ConfirmingBanUser(roomMember), eventSink = eventsRecorder ) rule.setRoomMembersModerationView( @@ -181,7 +182,7 @@ class RoomMembersModerationViewTest { val roomMember = anAlice() val state = aRoomMembersModerationState( selectedRoomMember = roomMember, - unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, + unbanUserAsyncAction = ConfirmingBanUser(roomMember), eventSink = eventsRecorder ) rule.setRoomMembersModerationView( @@ -189,7 +190,7 @@ class RoomMembersModerationViewTest { ) // Note: the string key semantics is not perfect here :/ rule.clickOn(R.string.screen_room_member_list_manage_member_unban_action) - eventsRecorder.assertSingle(RoomMembersModerationEvents.UnbanUser) + eventsRecorder.assertSingle(RoomMembersModerationEvents.UnbanUser(roomMember.userId)) } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt index 3dd81344fc..81ae3e6b89 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt +++ b/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.MessageEventType 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.canKick 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.canSendMessage @@ -62,6 +65,36 @@ fun MatrixRoom.canPinUnpin(updateKey: Long): State { } } +@Composable +fun MatrixRoom.isDmAsState(updateKey: Long): State { + return produceState(initialValue = false, key1 = updateKey) { + value = isDm + } +} + +@Composable +fun MatrixRoom.canKickAsState(updateKey: Long): State { + return produceState(initialValue = false, key1 = updateKey) { + value = canKick().getOrElse { false } + } +} + +@Composable +fun MatrixRoom.canBanAsState(updateKey: Long): State { + return produceState(initialValue = false, key1 = updateKey) { + value = canBan().getOrElse { false } + } +} + +@Composable +fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State { + return produceState(initialValue = 0, key1 = updateKey) { + value = userRole(sessionId) + .getOrDefault(RoomMember.Role.USER) + .powerLevel + } +} + @Composable fun MatrixRoom.isOwnUserAdmin(): Boolean { val roomInfo by roomInfoFlow.collectAsState(initial = null) From c66802b1c191525b9d862035473e6097cb98c420 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 16 Oct 2024 08:26:49 +0000 Subject: [PATCH 2/3] Update screenshots --- ....members.moderation_RoomMembersModerationView_Day_5_en.png | 4 ++-- ...embers.moderation_RoomMembersModerationView_Night_5_en.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en.png index dfd31916cb..1b6fb4bab8 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e0b84ff9cdbc6cc203304ff350789437533f9f7a1d95e8a196cce3585c454ec -size 9143 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en.png index 24e6d2e293..d6fd8eeb70 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b4fd072108b60d09d5c37c56e0e97272a1664ca5d21a27152e111539ac1a640 -size 7861 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 From b33b423a19a658b5b15bf3ab90ac3596a8f2749f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 16 Oct 2024 11:58:54 +0200 Subject: [PATCH 3/3] The name ConfirmingBanUser was wrong since it was used to confirm a unban action. Better to keep a generic name, in case this class is used for any other action. --- .../{ConfirmingBanUser.kt => ConfirmingRoomMemberAction.kt} | 2 +- .../members/moderation/RoomMembersModerationPresenter.kt | 4 +++- .../moderation/RoomMembersModerationStateProvider.kt | 2 +- .../impl/members/moderation/RoomMembersModerationView.kt | 2 +- .../moderation/RoomMembersModerationPresenterTest.kt | 6 +++--- .../members/moderation/RoomMembersModerationViewTest.kt | 6 +++--- 6 files changed, 12 insertions(+), 10 deletions(-) rename features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/{ConfirmingBanUser.kt => ConfirmingRoomMemberAction.kt} (91%) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingBanUser.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt similarity index 91% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingBanUser.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt index fbfa60b5cc..29e4e6b451 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingBanUser.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt @@ -10,6 +10,6 @@ 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( +data class ConfirmingRoomMemberAction( val roomMember: RoomMember, ) : AsyncAction.Confirming diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt index c025fa1bbc..700344b139 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt @@ -88,8 +88,10 @@ class RoomMembersModerationPresenter @Inject constructor( when (event) { is RoomMembersModerationEvents.SelectRoomMember -> { if (event.roomMember.membership == RoomMembershipState.BAN && canBan) { - unbanUserAsyncAction.value = ConfirmingBanUser(event.roomMember) + // In this case the view will render a dialog to confirm the unbanning of the user + unbanUserAsyncAction.value = ConfirmingRoomMemberAction(event.roomMember) } else { + // In this case the view will render a bottom sheet. selectedMember = event.roomMember } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt index 2c2fb6d27c..8f05d36da6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt @@ -60,7 +60,7 @@ class RoomMembersModerationStateProvider : PreviewParameterProvider { - if (action is ConfirmingBanUser) { + if (action is ConfirmingRoomMemberAction) { ConfirmationDialog( title = stringResource(R.string.screen_room_member_list_manage_member_unban_title), content = stringResource(R.string.screen_room_member_list_manage_member_unban_message), diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt index 1480f80716..423b93fc63 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt @@ -14,7 +14,7 @@ import com.google.common.truth.Truth.assertThat 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.aVictor -import io.element.android.features.roomdetails.impl.members.moderation.ConfirmingBanUser +import io.element.android.features.roomdetails.impl.members.moderation.ConfirmingRoomMemberAction 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.RoomMembersModerationPresenter @@ -153,7 +153,7 @@ class RoomMembersModerationPresenterTest { awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) with(awaitItem()) { assertThat(selectedRoomMember).isNull() - assertThat(unbanUserAsyncAction).isEqualTo(ConfirmingBanUser(selectedMember)) + assertThat(unbanUserAsyncAction).isEqualTo(ConfirmingRoomMemberAction(selectedMember)) } } } @@ -244,7 +244,7 @@ class RoomMembersModerationPresenterTest { val confirmingState = awaitItem() assertThat(confirmingState.selectedRoomMember).isNull() assertThat(confirmingState.actions).isEmpty() - assertThat(confirmingState.unbanUserAsyncAction).isEqualTo(ConfirmingBanUser(selectedMember)) + assertThat(confirmingState.unbanUserAsyncAction).isEqualTo(ConfirmingRoomMemberAction(selectedMember)) // Confirms unban confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(selectedMember.userId)) assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt index 41821411f9..02fdff8036 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 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.moderation.ConfirmingBanUser +import io.element.android.features.roomdetails.impl.members.moderation.ConfirmingRoomMemberAction 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.RoomMembersModerationState @@ -165,7 +165,7 @@ class RoomMembersModerationViewTest { val roomMember = anAlice() val state = aRoomMembersModerationState( selectedRoomMember = roomMember, - unbanUserAsyncAction = ConfirmingBanUser(roomMember), + unbanUserAsyncAction = ConfirmingRoomMemberAction(roomMember), eventSink = eventsRecorder ) rule.setRoomMembersModerationView( @@ -182,7 +182,7 @@ class RoomMembersModerationViewTest { val roomMember = anAlice() val state = aRoomMembersModerationState( selectedRoomMember = roomMember, - unbanUserAsyncAction = ConfirmingBanUser(roomMember), + unbanUserAsyncAction = ConfirmingRoomMemberAction(roomMember), eventSink = eventsRecorder ) rule.setRoomMembersModerationView(