From f2ff3269384900301096439c6e5e53c5ac4a0b01 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 13:21:11 +0200 Subject: [PATCH] Observe ignoredUsersFlow to have live data about blocked user. This will also ensure that blocking a user will work even if the user is not a member of the room (preparatory work for user permalink) --- .../details/RoomMemberDetailsPresenter.kt | 45 ++++++++----------- .../RoomMemberDetailsPresenterTests.kt | 31 +++++++++---- .../libraries/matrix/test/FakeMatrixClient.kt | 5 +++ 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index cb749d2c80..04c59c866f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -39,6 +39,10 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch class RoomMemberDetailsPresenter @AssistedInject constructor( @@ -57,14 +61,13 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( var confirmationDialog by remember { mutableStateOf(null) } val roomMember by room.getRoomMemberAsState(roomMemberId) val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } - // the room member is not really live... - val isBlocked: MutableState> = remember(roomMember) { - val isIgnored = roomMember?.isIgnored - if (isIgnored == null) { - mutableStateOf(AsyncData.Uninitialized) - } else { - mutableStateOf(AsyncData.Success(isIgnored)) - } + val isBlocked: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } + LaunchedEffect(Unit) { + client.ignoredUsersFlow + .map { ignoredUsers -> roomMemberId in ignoredUsers } + .distinctUntilChanged() + .onEach { isBlocked.value = AsyncData.Success(it) } + .launchIn(this) } LaunchedEffect(Unit) { // Update room member info when opening this screen @@ -132,28 +135,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState>) = launch { isBlockedState.value = AsyncData.Loading(false) client.ignoreUser(userId) - .fold( - onSuccess = { - isBlockedState.value = AsyncData.Success(true) - room.getUpdatedMember(userId) - }, - onFailure = { - isBlockedState.value = AsyncData.Failure(it, false) - } - ) + .onFailure { + isBlockedState.value = AsyncData.Failure(it, false) + } + // Note: on success, ignoredUserList will be updated. } private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState>) = launch { isBlockedState.value = AsyncData.Loading(true) client.unignoreUser(userId) - .fold( - onSuccess = { - isBlockedState.value = AsyncData.Success(false) - room.getUpdatedMember(userId) - }, - onFailure = { - isBlockedState.value = AsyncData.Failure(it, true) - } - ) + .onFailure { + isBlockedState.value = AsyncData.Failure(it, true) + } + // Note: on success, ignoredUserList will be updated. } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt index e1f0d76ec0..4d1d469f8f 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.members.details import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.createroom.api.StartDMAction @@ -63,7 +64,7 @@ class RoomMemberDetailsPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.userId).isEqualTo(roomMember.userId.value) assertThat(initialState.userName).isEqualTo(roomMember.displayName) assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) @@ -90,7 +91,7 @@ class RoomMemberDetailsPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.userName).isEqualTo(roomMember.displayName) assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) @@ -113,7 +114,7 @@ class RoomMemberDetailsPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.userName).isEqualTo(roomMember.displayName) assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) @@ -127,7 +128,7 @@ class RoomMemberDetailsPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) val dialogState = awaitItem() @@ -142,17 +143,24 @@ class RoomMemberDetailsPresenterTests { @Test fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { - val presenter = createRoomMemberDetailsPresenter() + val client = FakeMatrixClient() + val roomMember = aRoomMember() + val presenter = createRoomMemberDetailsPresenter( + client = client, + roomMember = roomMember, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() + client.emitIgnoreUserList(listOf(roomMember.userId)) assertThat(awaitItem().isBlocked.dataOrNull()).isTrue() initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() + client.emitIgnoreUserList(listOf()) assertThat(awaitItem().isBlocked.dataOrNull()).isFalse() } } @@ -165,7 +173,7 @@ class RoomMemberDetailsPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() val errorState = awaitItem() @@ -182,7 +190,7 @@ class RoomMemberDetailsPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) val dialogState = awaitItem() @@ -202,7 +210,7 @@ class RoomMemberDetailsPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID) val startDMFailureResult = AsyncAction.Failure(A_THROWABLE) @@ -229,6 +237,11 @@ class RoomMemberDetailsPresenterTests { } } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { + skipItems(1) + return awaitItem() + } + private fun createRoomMemberDetailsPresenter( client: MatrixClient = FakeMatrixClient(), room: MatrixRoom = aMatrixRoom(), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 7e86f29cb7..3feb161cb3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.test.verification.FakeSessionVerifica import io.element.android.tests.testutils.simulateLongTask import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -205,6 +206,10 @@ class FakeMatrixClient( return RoomMembershipObserver() } + suspend fun emitIgnoreUserList(users: List) { + ignoredUsersFlow.emit(users.toImmutableList()) + } + // Mocks fun givenLogoutError(failure: Throwable?) {