Browse Source

Remove dependencies to other presenters to RoomMembersModerationPresenter.

Move canDisplayModerationActions from presenter API to the state it emits.
pull/3618/head
Benoit Marty 2 weeks ago committed by Benoit Marty
parent
commit
2e5450c4b4
  1. 23
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt
  2. 6
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt
  3. 174
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt
  4. 184
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt
  5. 1
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt
  6. 2
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStatePreviewProvider.kt
  7. 29
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt
  8. 34
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/FakeRoomMembersModerationPresenter.kt
  9. 47
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt

23
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt

@ -0,0 +1,23 @@
/*
* 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.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.RoomScope
@Module
@ContributesTo(RoomScope::class)
interface RoomDetailsModule {
@Binds
fun bindRoomMembersModerationPresenter(presenter: RoomMembersModerationPresenter): Presenter<RoomMembersModerationState>
}

6
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt

@ -21,7 +21,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
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.RoomMembersModerationState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -40,7 +40,7 @@ class RoomMemberListPresenter @AssistedInject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val roomMemberListDataSource: RoomMemberListDataSource, private val roomMemberListDataSource: RoomMemberListDataSource,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val roomMembersModerationPresenter: RoomMembersModerationPresenter, private val roomMembersModerationPresenter: Presenter<RoomMembersModerationState>,
@Assisted private val navigator: RoomMemberListNavigator, @Assisted private val navigator: RoomMemberListNavigator,
) : Presenter<RoomMemberListState> { ) : Presenter<RoomMemberListState> {
@AssistedFactory @AssistedFactory
@ -136,7 +136,7 @@ class RoomMemberListPresenter @AssistedInject constructor(
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
is RoomMemberListEvents.RoomMemberSelected -> coroutineScope.launch { is RoomMemberListEvents.RoomMemberSelected -> coroutineScope.launch {
if (roomMembersModerationPresenter.canDisplayModerationActions()) { if (roomModerationState.canDisplayModerationActions) {
roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember)) roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember))
} else { } else {
navigator.openRoomMemberDetails(event.roomMember.userId) navigator.openRoomMemberDetails(event.roomMember.userId)

174
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt

@ -1,174 +0,0 @@
/*
* 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 androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
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
import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.RoomModeration
import io.element.android.libraries.architecture.AsyncAction
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.di.RoomScope
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.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import javax.inject.Inject
@ContributesBinding(RoomScope::class)
class DefaultRoomMembersModerationPresenter @Inject constructor(
private val room: MatrixRoom,
private val dispatchers: CoroutineDispatchers,
private val analyticsService: AnalyticsService,
) : RoomMembersModerationPresenter {
private var selectedMember by mutableStateOf<RoomMember?>(null)
private suspend fun canBan() = room.canBan().getOrDefault(false)
private suspend fun canKick() = room.canKick().getOrDefault(false)
override suspend fun canDisplayModerationActions(): Boolean {
return !room.isDm && (canBan() || canKick())
}
@Composable
override fun present(): RoomMembersModerationState {
val coroutineScope = rememberCoroutineScope()
var moderationActions by remember { mutableStateOf(persistentListOf<ModerationAction>()) }
val kickUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val banUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val canDisplayBannedUsers by produceState(initialValue = false) {
value = !room.isDm && canBan()
}
fun handleEvent(event: RoomMembersModerationEvents) {
when (event) {
is RoomMembersModerationEvents.SelectRoomMember -> {
coroutineScope.launch {
selectedMember = event.roomMember
if (event.roomMember.membership == RoomMembershipState.BAN && canBan()) {
unbanUserAsyncAction.value = AsyncAction.Confirming
} 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)
}
}
is RoomMembersModerationEvents.BanUser -> {
if (banUserAsyncAction.value.isConfirming()) {
moderationActions = persistentListOf()
selectedMember?.let {
coroutineScope.banUser(it.userId, banUserAsyncAction)
}
} else {
banUserAsyncAction.value = AsyncAction.Confirming
}
}
is RoomMembersModerationEvents.UnbanUser -> {
if (unbanUserAsyncAction.value.isConfirming()) {
moderationActions = persistentListOf()
selectedMember?.let {
coroutineScope.unbanUser(it.userId, unbanUserAsyncAction)
}
} else {
unbanUserAsyncAction.value = AsyncAction.Confirming
}
}
is RoomMembersModerationEvents.Reset -> {
selectedMember = null
moderationActions = persistentListOf()
kickUserAsyncAction.value = AsyncAction.Uninitialized
banUserAsyncAction.value = AsyncAction.Uninitialized
unbanUserAsyncAction.value = AsyncAction.Uninitialized
}
}
}
return RoomMembersModerationState(
selectedRoomMember = selectedMember,
actions = moderationActions,
kickUserAsyncAction = kickUserAsyncAction.value,
banUserAsyncAction = banUserAsyncAction.value,
unbanUserAsyncAction = unbanUserAsyncAction.value,
canDisplayBannedUsers = canDisplayBannedUsers,
eventSink = { handleEvent(it) },
)
}
private fun CoroutineScope.kickUser(
userId: UserId,
kickUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(kickUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember))
room.kickUser(userId).finally { selectedMember = null }
}
private fun CoroutineScope.banUser(
userId: UserId,
banUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(banUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember))
room.banUser(userId).finally { selectedMember = null }
}
private fun CoroutineScope.unbanUser(
userId: UserId,
unbanUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(unbanUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
room.unbanUser(userId).finally { selectedMember = null }
}
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(action: MutableState<AsyncAction<T>>, block: suspend () -> Result<T>) {
launch(dispatchers.io) {
action.runUpdatingState {
val result = block()
if (result.isSuccess) {
room.membersStateFlow.drop(1).take(1)
}
result
}
}
}
}

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

@ -7,20 +7,180 @@
package io.element.android.features.roomdetails.impl.members.moderation 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.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
import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.RoomModeration
import io.element.android.libraries.architecture.AsyncAction 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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.finally
import io.element.android.libraries.di.RoomScope
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.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import javax.inject.Inject
interface RoomMembersModerationPresenter : Presenter<RoomMembersModerationState> { class RoomMembersModerationPresenter @Inject constructor(
suspend fun canDisplayModerationActions(): Boolean private val room: MatrixRoom,
private val dispatchers: CoroutineDispatchers,
fun dummyState() = RoomMembersModerationState( private val analyticsService: AnalyticsService,
selectedRoomMember = null, ) : Presenter<RoomMembersModerationState> {
actions = persistentListOf(), private var selectedMember by mutableStateOf<RoomMember?>(null)
kickUserAsyncAction = AsyncAction.Uninitialized,
banUserAsyncAction = AsyncAction.Uninitialized, private suspend fun canBan() = room.canBan().getOrDefault(false)
unbanUserAsyncAction = AsyncAction.Uninitialized, private suspend fun canKick() = room.canKick().getOrDefault(false)
canDisplayBannedUsers = false,
eventSink = {} @Composable
) override fun present(): RoomMembersModerationState {
val coroutineScope = rememberCoroutineScope()
var moderationActions by remember { mutableStateOf(persistentListOf<ModerationAction>()) }
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val canDisplayModerationActions by produceState(
initialValue = false,
key1 = syncUpdateFlow.value
) {
value = !room.isDm && (canBan() || canKick())
}
val kickUserAsyncAction =
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val banUserAsyncAction =
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val unbanUserAsyncAction =
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val canDisplayBannedUsers by produceState(initialValue = false) {
value = !room.isDm && canBan()
}
fun handleEvent(event: RoomMembersModerationEvents) {
when (event) {
is RoomMembersModerationEvents.SelectRoomMember -> {
coroutineScope.launch {
selectedMember = event.roomMember
if (event.roomMember.membership == RoomMembershipState.BAN && canBan()) {
unbanUserAsyncAction.value = AsyncAction.Confirming
} 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)
}
}
is RoomMembersModerationEvents.BanUser -> {
if (banUserAsyncAction.value.isConfirming()) {
moderationActions = persistentListOf()
selectedMember?.let {
coroutineScope.banUser(it.userId, banUserAsyncAction)
}
} else {
banUserAsyncAction.value = AsyncAction.Confirming
}
}
is RoomMembersModerationEvents.UnbanUser -> {
if (unbanUserAsyncAction.value.isConfirming()) {
moderationActions = persistentListOf()
selectedMember?.let {
coroutineScope.unbanUser(it.userId, unbanUserAsyncAction)
}
} else {
unbanUserAsyncAction.value = AsyncAction.Confirming
}
}
is RoomMembersModerationEvents.Reset -> {
selectedMember = null
moderationActions = persistentListOf()
kickUserAsyncAction.value = AsyncAction.Uninitialized
banUserAsyncAction.value = AsyncAction.Uninitialized
unbanUserAsyncAction.value = AsyncAction.Uninitialized
}
}
}
return RoomMembersModerationState(
canDisplayModerationActions = canDisplayModerationActions,
selectedRoomMember = selectedMember,
actions = moderationActions,
kickUserAsyncAction = kickUserAsyncAction.value,
banUserAsyncAction = banUserAsyncAction.value,
unbanUserAsyncAction = unbanUserAsyncAction.value,
canDisplayBannedUsers = canDisplayBannedUsers,
eventSink = { handleEvent(it) },
)
}
private fun CoroutineScope.kickUser(
userId: UserId,
kickUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(kickUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember))
room.kickUser(userId).finally { selectedMember = null }
}
private fun CoroutineScope.banUser(
userId: UserId,
banUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(banUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember))
room.banUser(userId).finally { selectedMember = null }
}
private fun CoroutineScope.unbanUser(
userId: UserId,
unbanUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(unbanUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
room.unbanUser(userId).finally { selectedMember = null }
}
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(
action: MutableState<AsyncAction<T>>,
block: suspend () -> Result<T>
) {
launch(dispatchers.io) {
action.runUpdatingState {
val result = block()
if (result.isSuccess) {
room.membersStateFlow.drop(1).take(1)
}
result
}
}
}
} }

1
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class RoomMembersModerationState( data class RoomMembersModerationState(
val canDisplayModerationActions: Boolean,
val selectedRoomMember: RoomMember?, val selectedRoomMember: RoomMember?,
val actions: ImmutableList<ModerationAction>, val actions: ImmutableList<ModerationAction>,
val kickUserAsyncAction: AsyncAction<Unit>, val kickUserAsyncAction: AsyncAction<Unit>,

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

@ -71,6 +71,7 @@ class RoomMembersModerationStatePreviewProvider : PreviewParameterProvider<RoomM
} }
fun aRoomMembersModerationState( fun aRoomMembersModerationState(
canDisplayModerationActions: Boolean = false,
selectedRoomMember: RoomMember? = null, selectedRoomMember: RoomMember? = null,
actions: List<ModerationAction> = emptyList(), actions: List<ModerationAction> = emptyList(),
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized, kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
@ -79,6 +80,7 @@ fun aRoomMembersModerationState(
canDisplayBannedUsers: Boolean = false, canDisplayBannedUsers: Boolean = false,
eventSink: (RoomMembersModerationEvents) -> Unit = {}, eventSink: (RoomMembersModerationEvents) -> Unit = {},
) = RoomMembersModerationState( ) = RoomMembersModerationState(
canDisplayModerationActions = canDisplayModerationActions,
selectedRoomMember = selectedRoomMember, selectedRoomMember = selectedRoomMember,
actions = actions.toPersistentList(), actions = actions.toPersistentList(),
kickUserAsyncAction = kickUserAsyncAction, kickUserAsyncAction = kickUserAsyncAction,

29
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt

@ -19,8 +19,8 @@ import io.element.android.features.roomdetails.impl.members.aRoomMemberList
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.aWalter import io.element.android.features.roomdetails.impl.members.aWalter
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.aRoomMembersModerationState import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
import io.element.android.features.roomdetails.members.moderation.FakeRoomMembersModerationPresenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -194,9 +195,9 @@ class RoomMemberListPresenterTest {
@Test @Test
fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest { fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest {
val navigator = FakeRoomMemberListNavigator() val navigator = FakeRoomMemberListNavigator()
val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = false) val roomMembersModerationStateLambda = { aRoomMembersModerationState(canDisplayModerationActions = false) }
val presenter = createPresenter( val presenter = createPresenter(
moderationPresenter = moderationPresenter, roomMembersModerationStateLambda = roomMembersModerationStateLambda,
navigator = navigator, navigator = navigator,
matrixRoom = FakeMatrixRoom( matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) }, updateMembersResult = { Result.success(Unit) },
@ -215,17 +216,15 @@ class RoomMemberListPresenterTest {
@Test @Test
fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest { fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest {
val navigator = FakeRoomMemberListNavigator() val navigator = FakeRoomMemberListNavigator()
var selectRoomMemberCallCounts = 0 val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
val capturingState = aRoomMembersModerationState(eventSink = { event -> val roomMembersModerationStateLambda = {
if (event is RoomMembersModerationEvents.SelectRoomMember) { aRoomMembersModerationState(
selectRoomMemberCallCounts++ canDisplayModerationActions = true,
} eventSink = eventsRecorder,
}) )
val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = true).apply {
givenState(capturingState)
} }
val presenter = createPresenter( val presenter = createPresenter(
moderationPresenter = moderationPresenter, roomMembersModerationStateLambda = roomMembersModerationStateLambda,
navigator = navigator, navigator = navigator,
matrixRoom = FakeMatrixRoom( matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) }, updateMembersResult = { Result.success(Unit) },
@ -237,7 +236,7 @@ class RoomMemberListPresenterTest {
}.test { }.test {
skipItems(1) skipItems(1)
awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor()))
assertThat(selectRoomMemberCallCounts).isEqualTo(1) eventsRecorder.assertSingle(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
} }
} }
} }
@ -266,12 +265,12 @@ private fun TestScope.createPresenter(
updateMembersResult = { Result.success(Unit) } updateMembersResult = { Result.success(Unit) }
), ),
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers),
moderationPresenter: FakeRoomMembersModerationPresenter = FakeRoomMembersModerationPresenter(), roomMembersModerationStateLambda: () -> RoomMembersModerationState = { aRoomMembersModerationState() },
navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {} navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {}
) = RoomMemberListPresenter( ) = RoomMemberListPresenter(
room = matrixRoom, room = matrixRoom,
roomMemberListDataSource = roomMemberListDataSource, roomMemberListDataSource = roomMemberListDataSource,
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
roomMembersModerationPresenter = moderationPresenter, roomMembersModerationPresenter = { roomMembersModerationStateLambda() },
navigator = navigator navigator = navigator
) )

34
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/FakeRoomMembersModerationPresenter.kt

@ -1,34 +0,0 @@
/*
* 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.members.moderation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
class FakeRoomMembersModerationPresenter(
private val canDisplayModerationActions: Boolean = true,
) : RoomMembersModerationPresenter {
private var state by mutableStateOf(dummyState())
override suspend fun canDisplayModerationActions(): Boolean {
return canDisplayModerationActions
}
@Composable
override fun present(): RoomMembersModerationState {
return state
}
fun givenState(state: RoomMembersModerationState) {
this.state = state
}
}

47
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTest.kt → features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt

@ -14,9 +14,9 @@ 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.DefaultRoomMembersModerationPresenter
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.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@ -27,20 +27,23 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class DefaultRoomMembersModerationPresenterTest { 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).apply {
givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2)) givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2))
} }
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
assertThat(presenter.canDisplayModerationActions()).isFalse() presenter.test {
assertThat(awaitItem().canDisplayModerationActions).isFalse()
}
} }
@Test @Test
@ -51,8 +54,11 @@ class DefaultRoomMembersModerationPresenterTest {
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
assertThat(presenter.canDisplayModerationActions()).isTrue() presenter.test {
skipItems(1)
assertThat(awaitItem().canDisplayModerationActions).isTrue()
}
} }
@Test @Test
@ -62,8 +68,11 @@ class DefaultRoomMembersModerationPresenterTest {
activeMemberCount = 10, activeMemberCount = 10,
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
assertThat(presenter.canDisplayModerationActions()).isTrue() presenter.test {
skipItems(1)
assertThat(awaitItem().canDisplayModerationActions).isTrue()
}
} }
@Test @Test
@ -74,7 +83,7 @@ class DefaultRoomMembersModerationPresenterTest {
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
) )
val selectedMember = aVictor() val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -101,7 +110,7 @@ class DefaultRoomMembersModerationPresenterTest {
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
) )
val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L) val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L)
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -125,7 +134,7 @@ class DefaultRoomMembersModerationPresenterTest {
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -148,7 +157,7 @@ class DefaultRoomMembersModerationPresenterTest {
kickUserResult = { _, _ -> Result.success(Unit) }, kickUserResult = { _, _ -> Result.success(Unit) },
) )
val selectedMember = aVictor() val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -176,7 +185,7 @@ class DefaultRoomMembersModerationPresenterTest {
banUserResult = { _, _ -> Result.success(Unit) }, banUserResult = { _, _ -> Result.success(Unit) },
) )
val selectedMember = aVictor() val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -211,7 +220,7 @@ class DefaultRoomMembersModerationPresenterTest {
).apply { ).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember))) givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember)))
} }
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -237,7 +246,7 @@ class DefaultRoomMembersModerationPresenterTest {
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.USER) }, userRoleResult = { Result.success(RoomMember.Role.USER) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -261,7 +270,7 @@ class DefaultRoomMembersModerationPresenterTest {
unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
userRoleResult = { Result.success(RoomMember.Role.USER) }, userRoleResult = { Result.success(RoomMember.Role.USER) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -301,12 +310,12 @@ class DefaultRoomMembersModerationPresenterTest {
} }
} }
private fun TestScope.createDefaultRoomMembersModerationPresenter( private fun TestScope.createRoomMembersModerationPresenter(
matrixRoom: FakeMatrixRoom = FakeMatrixRoom(), matrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
): DefaultRoomMembersModerationPresenter { ): RoomMembersModerationPresenter {
return DefaultRoomMembersModerationPresenter( return RoomMembersModerationPresenter(
room = matrixRoom, room = matrixRoom,
dispatchers = dispatchers, dispatchers = dispatchers,
analyticsService = analyticsService, analyticsService = analyticsService,
Loading…
Cancel
Save