From f2ff3269384900301096439c6e5e53c5ac4a0b01 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 13:21:11 +0200 Subject: [PATCH 1/8] 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?) { From c9d59616843a30e7891c145cf612b11d85be87ba Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 11:44:28 +0200 Subject: [PATCH 2/8] RoomMemberDetailsPresenter: fallback to user profile data if the user is not a member of the room. This can be displayed when the user click on a non-member user permalink. --- .../details/RoomMemberDetailsPresenter.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 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 04c59c866f..81263b504c 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 @@ -21,7 +21,6 @@ import androidx.compose.runtime.LaunchedEffect 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 @@ -37,6 +36,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId 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.user.MatrixUser import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged @@ -60,6 +60,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() var confirmationDialog by remember { mutableStateOf(null) } val roomMember by room.getRoomMemberAsState(roomMemberId) + var userProfile by remember { mutableStateOf(null) } val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val isBlocked: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } LaunchedEffect(Unit) { @@ -74,6 +75,9 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( // We don't need to assign the result as it will be automatically propagated by `room.getRoomMemberAsState` room.getUpdatedMember(roomMemberId) } + LaunchedEffect(Unit) { + userProfile = client.getProfile(roomMemberId).getOrNull() + } fun handleEvents(event: RoomMemberDetailsEvents) { when (event) { @@ -108,16 +112,26 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( } } - val userName by produceState(initialValue = roomMember?.displayName) { - room.userDisplayName(roomMemberId).onSuccess { displayName -> - if (displayName != null) value = displayName - } + var userName: String? by remember { mutableStateOf(roomMember?.displayName ?: userProfile?.displayName) } + LaunchedEffect(roomMember, userProfile) { + userName = room.userDisplayName(roomMemberId) + .fold( + onSuccess = { it }, + onFailure = { + // Fallback to user profile + userProfile?.displayName + }) } - val userAvatar by produceState(initialValue = roomMember?.avatarUrl) { - room.userAvatarUrl(roomMemberId).onSuccess { avatarUrl -> - if (avatarUrl != null) value = avatarUrl - } + var userAvatar: String? by remember { mutableStateOf(roomMember?.avatarUrl ?: userProfile?.avatarUrl) } + LaunchedEffect(roomMember, userProfile) { + userAvatar = room.userAvatarUrl(roomMemberId) + .fold( + onSuccess = { it }, + onFailure = { + // Fallback to user profile + userProfile?.avatarUrl + }) } return RoomMemberDetailsState( @@ -127,7 +141,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( isBlocked = isBlocked.value, startDmActionState = startDmActionState.value, displayConfirmationDialog = confirmationDialog, - isCurrentUser = client.isMe(roomMember?.userId), + isCurrentUser = client.isMe(roomMemberId), eventSink = ::handleEvents ) } From 0dba614c0a8432ac427efb11b233b58bbc9318f9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 13:35:16 +0200 Subject: [PATCH 3/8] createRoomMemberDetailsPresenter just need a UserId. --- .../details/RoomMemberDetailsPresenterTests.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 4d1d469f8f..4eefdd100a 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 @@ -31,9 +31,9 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.MatrixClient +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.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -59,7 +59,7 @@ class RoomMemberDetailsPresenterTests { } val presenter = createRoomMemberDetailsPresenter( room = room, - roomMember = roomMember + roomMemberId = roomMember.userId ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -86,7 +86,7 @@ class RoomMemberDetailsPresenterTests { } val presenter = createRoomMemberDetailsPresenter( room = room, - roomMember = roomMember + roomMemberId = roomMember.userId ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -109,7 +109,7 @@ class RoomMemberDetailsPresenterTests { } val presenter = createRoomMemberDetailsPresenter( room = room, - roomMember = roomMember + roomMemberId = roomMember.userId ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -147,7 +147,7 @@ class RoomMemberDetailsPresenterTests { val roomMember = aRoomMember() val presenter = createRoomMemberDetailsPresenter( client = client, - roomMember = roomMember, + roomMemberId = roomMember.userId ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -245,11 +245,11 @@ class RoomMemberDetailsPresenterTests { private fun createRoomMemberDetailsPresenter( client: MatrixClient = FakeMatrixClient(), room: MatrixRoom = aMatrixRoom(), - roomMember: RoomMember = aRoomMember(), + roomMemberId: UserId = UserId("@alice:server.org"), startDMAction: StartDMAction = FakeStartDMAction() ): RoomMemberDetailsPresenter { return RoomMemberDetailsPresenter( - roomMemberId = roomMember.userId, + roomMemberId = roomMemberId, client = client, room = room, startDMAction = startDMAction From 4d50e43e85bdd56b317d4365c53a25b1a9bc67af Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 13:42:03 +0200 Subject: [PATCH 4/8] Add test covering fallback to user profile. --- .../RoomMemberDetailsPresenterTests.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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 4eefdd100a..428ececea0 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 @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.tests.testutils.WarmUpRule import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -122,6 +123,33 @@ class RoomMemberDetailsPresenterTests { } } + @Test + fun `present - will fallback to user profile if user is not a member of the room`() = runTest { + val bobProfile = aMatrixUser("@bob:server.org", "Bob", avatarUrl = "anAvatarUrl") + val room = aMatrixRoom().apply { + givenUserDisplayNameResult(Result.failure(Exception("Not a member!"))) + givenUserAvatarUrlResult(Result.failure(Exception("Not a member!"))) + } + val client = FakeMatrixClient().apply { + givenGetProfileResult(bobProfile.userId, Result.success(bobProfile)) + } + val presenter = createRoomMemberDetailsPresenter( + client = client, + room = room, + roomMemberId = UserId("@bob:server.org") + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(2) + val initialState = awaitFirstItem() + assertThat(initialState.userName).isEqualTo("Bob") + assertThat(initialState.avatarUrl).isEqualTo("anAvatarUrl") + + ensureAllEventsConsumed() + } + } + @Test fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { val presenter = createRoomMemberDetailsPresenter() From 47e1d6fb0cd3f1502459d78c614e6d242955077c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 13:43:17 +0200 Subject: [PATCH 5/8] Changelog --- changelog.d/2721.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2721.misc diff --git a/changelog.d/2721.misc b/changelog.d/2721.misc new file mode 100644 index 0000000000..2c38dc4ac7 --- /dev/null +++ b/changelog.d/2721.misc @@ -0,0 +1 @@ +RoomMember screen: fallback to userProfile data, if the member is not a user of the room. From 3f0159d4387fa4cb9e18d3ce819dbd18ed093304 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 14:01:03 +0200 Subject: [PATCH 6/8] Format --- .../impl/members/details/RoomMemberDetailsPresenter.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 81263b504c..5c98a048ff 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 @@ -120,7 +120,8 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( onFailure = { // Fallback to user profile userProfile?.displayName - }) + } + ) } var userAvatar: String? by remember { mutableStateOf(roomMember?.avatarUrl ?: userProfile?.avatarUrl) } @@ -131,7 +132,8 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( onFailure = { // Fallback to user profile userProfile?.avatarUrl - }) + } + ) } return RoomMemberDetailsState( From f9d3f947c2cbd1654ad35e20287287476a36f5b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 14:44:16 +0200 Subject: [PATCH 7/8] Add missing test for unblock user with error. --- .../RoomMemberDetailsPresenterTests.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 428ececea0..4c8b944c8a 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 @@ -212,6 +212,25 @@ class RoomMemberDetailsPresenterTests { } } + @Test + fun `present - UnblockUser with error`() = runTest { + val matrixClient = FakeMatrixClient() + matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE)) + val presenter = createRoomMemberDetailsPresenter(client = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) + assertThat(awaitItem().isBlocked.isLoading()).isTrue() + val errorState = awaitItem() + assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE) + // Clear error + initialState.eventSink(RoomMemberDetailsEvents.ClearBlockUserError) + assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true)) + } + } + @Test fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { val presenter = createRoomMemberDetailsPresenter() From 5b3a2d8ecafc79993f5c76f934c99d07bc664002 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Apr 2024 16:15:36 +0200 Subject: [PATCH 8/8] Use produceState and fetch profile only if necessary. --- .../details/RoomMemberDetailsPresenter.kt | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 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 5c98a048ff..dcb953e978 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 @@ -21,6 +21,7 @@ import androidx.compose.runtime.LaunchedEffect 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 @@ -74,9 +75,10 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( // Update room member info when opening this screen // We don't need to assign the result as it will be automatically propagated by `room.getRoomMemberAsState` room.getUpdatedMember(roomMemberId) - } - LaunchedEffect(Unit) { - userProfile = client.getProfile(roomMemberId).getOrNull() + .onFailure { + // Not a member of the room, try to get the user profile + userProfile = client.getProfile(roomMemberId).getOrNull() + } } fun handleEvents(event: RoomMemberDetailsEvents) { @@ -112,9 +114,12 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( } } - var userName: String? by remember { mutableStateOf(roomMember?.displayName ?: userProfile?.displayName) } - LaunchedEffect(roomMember, userProfile) { - userName = room.userDisplayName(roomMemberId) + val userName: String? by produceState( + initialValue = roomMember?.displayName ?: userProfile?.displayName, + key1 = roomMember, + key2 = userProfile, + ) { + value = room.userDisplayName(roomMemberId) .fold( onSuccess = { it }, onFailure = { @@ -124,9 +129,12 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( ) } - var userAvatar: String? by remember { mutableStateOf(roomMember?.avatarUrl ?: userProfile?.avatarUrl) } - LaunchedEffect(roomMember, userProfile) { - userAvatar = room.userAvatarUrl(roomMemberId) + val userAvatar: String? by produceState( + initialValue = roomMember?.avatarUrl ?: userProfile?.avatarUrl, + key1 = roomMember, + key2 = userProfile, + ) { + value = room.userAvatarUrl(roomMemberId) .fold( onSuccess = { it }, onFailure = {