From 19c6526841817dcc8ab79f0a5880345195b9dd0e Mon Sep 17 00:00:00 2001 From: Eklavya Jandial Date: Mon, 10 Aug 2026 13:10:12 +0530 Subject: [PATCH] [Task] Show active call participants in the people list #7199 (#7300) --- .../impl/members/RoomMemberListPresenter.kt | 46 ++++++++++++++----- .../impl/members/RoomMemberListState.kt | 1 + .../members/RoomMemberListStateProvider.kt | 8 +++- .../impl/members/RoomMemberListView.kt | 9 ++++ .../members/RoomMemberListPresenterTest.kt | 32 +++++++++++++ ...pl.members_RoomMemberListView_Day_2_en.png | 4 +- ...pl.members_RoomMemberListView_Day_4_en.png | 4 +- ....members_RoomMemberListView_Night_2_en.png | 4 +- ....members_RoomMemberListView_Night_4_en.png | 4 +- 9 files changed, 91 insertions(+), 21 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 f47f12f446..674dd9aad9 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 @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList @@ -58,6 +59,7 @@ class RoomMemberListPresenter( override fun present(): RoomMemberListState { val searchQuery = rememberTextFieldState() val membersState by room.membersStateFlow.collectAsState() + val roomInfo by room.roomInfoFlow.collectAsState() val canInvite by room.permissionsAsState(false) { perms -> perms.canOwnUserInvite() } val roomModerationState = roomMembersModerationPresenter.present() @@ -72,13 +74,18 @@ class RoomMemberListPresenter( var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS) } var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } var filteredRoomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } + val activeCallParticipants = roomInfo.activeRoomCallParticipants + + val comparator = remember(activeCallParticipants) { + roomMemberListComparator(activeCallParticipants) + } // Update the room members when the screen is loaded LaunchedEffect(Unit) { room.updateMembers() } - LaunchedEffect(membersState, roomMemberIdentityStates) { + LaunchedEffect(membersState, roomMemberIdentityStates, activeCallParticipants) { if (membersState is RoomMembersState.Unknown) { return@LaunchedEffect } @@ -90,23 +97,23 @@ class RoomMemberListPresenter( } withContext(coroutineDispatchers.io) { val members = membersState.roomMembers().orEmpty().groupBy { it.membership } - val info = room.info() - if (members.getOrDefault(RoomMembershipState.JOIN, emptyList()).size < info.joinedMembersCount / 2) { + if (members.getOrDefault(RoomMembershipState.JOIN, emptyList()).size < roomInfo.joinedMembersCount / 2) { // Don't display initial room member list if we have less than half of the joined members: - // This result will come from the timeline loading membership events and it'll be wrong. + // This result will come from the timeline loading membership events, and it'll be wrong. return@withContext } val result = RoomMembers( invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()) - .map { it.withIdentityState(roomMemberIdentityStates) } + .map { it.withIdentityState(roomMemberIdentityStates, activeCallParticipants) } .toImmutableList(), joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()) - .sortedWith(powerLevelRoomMemberComparator) - .map { it.withIdentityState(roomMemberIdentityStates) } + .sortedWith(comparator) + // .sortedWith(powerLevelRoomMemberComparator) + .map { it.withIdentityState(roomMemberIdentityStates, activeCallParticipants) } .toImmutableList(), banned = members.getOrDefault(RoomMembershipState.BAN, emptyList()) .sortedBy { it.userId.value } - .map { it.withIdentityState(roomMemberIdentityStates) } + .map { it.withIdentityState(roomMemberIdentityStates, activeCallParticipants) } .toImmutableList(), ) roomMembers = if (membersState is RoomMembersState.Pending) { @@ -150,12 +157,29 @@ class RoomMemberListPresenter( return state } - private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap): RoomMemberWithIdentityState { + private suspend fun RoomMember.withIdentityState( + identityStates: ImmutableMap, + activeCallParticipants: ImmutableList + ): RoomMemberWithIdentityState { return if (room.info().isEncrypted != true) { - RoomMemberWithIdentityState(this, null) + RoomMemberWithIdentityState(this, null, userId in activeCallParticipants) } else { val identityState = identityStates[userId] ?: encryptionService.getUserIdentity(userId, fallbackToServer = false).getOrNull() - RoomMemberWithIdentityState(this, identityState) + RoomMemberWithIdentityState(this, identityState, userId in activeCallParticipants) } } + + private fun roomMemberListComparator( + activeCallParticipants: List, + ): Comparator = + Comparator { a, b -> + val aInCall = a.userId in activeCallParticipants + val bInCall = b.userId in activeCallParticipants + + when { + aInCall && !bInCall -> -1 + !aInCall && bInCall -> 1 + else -> powerLevelRoomMemberComparator.compare(a, b) + } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index 6d52db6c87..7b6744ef0b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -66,4 +66,5 @@ data class RoomMembers( data class RoomMemberWithIdentityState( val roomMember: RoomMember, val identityState: IdentityState?, + val isInCall: Boolean ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 472a185076..6b1971fdae 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -78,7 +78,7 @@ private fun aLoadedRoomMembers() = AsyncData.Success( joined = persistentListOf( anAlice().withIdentity(identityState = IdentityState.Verified), aBob().withIdentity(identityState = IdentityState.PinViolation), - aCarol().withIdentity(), + aCarol().withIdentity(isInCall = true), aDavid().withIdentity(), anEve().withIdentity(identityState = IdentityState.VerificationViolation) ), @@ -174,4 +174,8 @@ fun aBannedSusie(): RoomMember = aRoomMember(UserId("@susie:server.org"), USER_N fun aBannedMallory(): RoomMember = aRoomMember(UserId("@mallory:server.org"), USER_NAME_MALLORY, membership = RoomMembershipState.BAN) -private fun RoomMember.withIdentity(identityState: IdentityState? = null) = RoomMemberWithIdentityState(this, identityState) +private fun RoomMember.withIdentity(identityState: IdentityState? = null, isInCall: Boolean = false) = RoomMemberWithIdentityState( + this, + identityState, + isInCall +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 659d8ed0e0..33ca61b570 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -338,6 +338,15 @@ private fun RoomMemberListItem( color = ElementTheme.colors.textSecondary, ) } + + if (roomMemberWithIdentity.isInCall) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.VideoCallSolid(), + contentDescription = stringResource(CommonStrings.action_video_call), + tint = ElementTheme.colors.iconSuccessPrimary + ) + } } } ) 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 48f0e791a3..0a88d2bf57 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 @@ -191,6 +191,38 @@ class RoomMemberListPresenterTest { awaitItem().eventSink(RoomMemberListEvent.RoomMemberSelected(anInvitedVictor())) } } + + @Test + fun `call participants are shown first in joined members`() = runTest { + val room = createFakeJoinedRoom().apply { + givenRoomInfo( + aRoomInfo( + joinedMembersCount = 5, + activeRoomCallParticipants = listOf( + aCarol().userId + ), + ) + ) + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + } + + val presenter = createPresenter(joinedRoom = room) + + presenter.test { + skipItems(1) + + val loadedState = awaitItem() + val joined = loadedState.filteredRoomMembers.dataOrNull()!!.joined + + assertThat(joined.first().roomMember.userId) + .isEqualTo(aCarol().userId) + + assertThat(joined.first().isInCall).isTrue() + + assertThat(joined.count { it.isInCall }).isEqualTo(1) + assertThat(joined.drop(1).all { !it.isInCall }).isTrue() + } + } } private fun createFakeJoinedRoom( diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_2_en.png index 54d2ed2f1d..51141ade27 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ceb07377e46e28001e0ce26daef88f65f4fd60be2debcf83672cf707163e76d7 -size 52992 +oid sha256:0015d03ba03fcca2ed52457efd13d87753be16d25fc7e54f0dd86d0d6b8ff77b +size 53289 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_4_en.png index 586e1de468..70b5529856 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:415488496b429f2fd1aa238b006e05198b0fc859286dd1cdd8ae7d8de1d3c3b3 -size 53851 +oid sha256:27e2e7ef3ec68ed0a339fe27bbe631b95597291517f26987929b44f924aed06a +size 54149 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_2_en.png index d4998f8375..814ac8df84 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d105ddd5bcc44641857376c227caf9124569fefc1b20b75d7a82e104055bd08 -size 53319 +oid sha256:7753e817d169d4e173cc234bf2a58ddbe8d29f45b164a1800025b38c1b83b77c +size 53569 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_4_en.png index 9e64d6b01e..d1f6b5493f 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3804c27513c81bf362309397e568cb57b2748aa4837364d0d30cfa28e07f9f7d -size 54155 +oid sha256:54909ba782c4ded91503b5b3b46a085f98a9480489a634c2ad1c88ffbf948083 +size 54404