From 1343aaed200bba2ced29f86a210ea8a9c05584c7 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 6 Aug 2025 12:37:52 +0200 Subject: [PATCH] Reload room member list when active members count changes (#5129) --- .../impl/members/RoomMemberListPresenter.kt | 11 +++- .../members/RoomMemberListPresenterTest.kt | 61 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 56268e6952..fac98da36f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -38,8 +38,10 @@ import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentMap +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import javax.inject.Inject @@ -64,6 +66,11 @@ class RoomMemberListPresenter @Inject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canInvite by room.canInviteAsState(syncUpdateFlow.value) val roomModerationState = roomMembersModerationPresenter.present() + val activeRoomMemberCount by produceState(0L) { + room.roomInfoFlow.map { it.activeMembersCount } + .distinctUntilChanged() + .collect { value = it } + } val roomMemberIdentityStates by produceState(persistentMapOf()) { room.roomMemberIdentityStateChange(waitForEncryption = true) @@ -73,8 +80,8 @@ class RoomMemberListPresenter @Inject constructor( .launchIn(this) } - // Ensure we load the latest data when entering this screen - LaunchedEffect(Unit) { + // Update the room members when the screen is loaded or the active member count changes + LaunchedEffect(activeRoomMemberCount) { room.updateMembers() } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt index aff05a55e6..0491c5d437 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt @@ -23,12 +23,20 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.time.withTimeout +import kotlinx.coroutines.withTimeout import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.seconds @ExperimentalCoroutinesApi class RoomMemberListPresenterTest { @@ -67,6 +75,59 @@ class RoomMemberListPresenterTest { } } + @Test + fun `member loading is done automatically when RoomInfo's activeMemberCount changes`() = runTest { + val reloadMembersMutex = Mutex() + val updateMembersLambda = lambdaRecorder { + if (reloadMembersMutex.isLocked) { + reloadMembersMutex.unlock() + } + } + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + updateMembersResult = updateMembersLambda, + canInviteResult = { Result.success(true) } + ).apply { + // Needed to avoid discarding the loaded members as a partial and invalid result + givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) + } + ) + val presenter = createPresenter(joinedRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.roomMembers.isLoading()).isTrue() + room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + // Skip item while the new members state is processed + skipItems(1) + val loadedMembersState = awaitItem() + assertThat(loadedMembersState.roomMembers.isLoading()).isFalse() + assertThat(loadedMembersState.roomMembers.dataOrNull()?.joined).isNotEmpty() + + // Assert no events are emitted only with that change + expectNoEvents() + + // This will only progress if the `Room.updateMembers()` function is called, triggered by the RoomInfo change + withTimeout(10.seconds) { + reloadMembersMutex.withLock { + launch { room.givenRoomInfo(aRoomInfo(activeMembersCount = 0L)) } + } + } + + // Wait for the update to be processed + skipItems(1) + + // Update the room members state as `Room.updateMembers()` would have done with the actual implementation + room.givenRoomMembersState(RoomMembersState.Ready(persistentListOf())) + // Wait for another update + skipItems(1) + // The members should be reloaded now + assertThat(awaitItem().roomMembers.dataOrNull()?.joined).isEmpty() + } + } + @Test fun `open search`() = runTest { val presenter = createPresenter(