[Task] Show active call participants in the people list #7199 (#7300)

This commit is contained in:
Eklavya Jandial
2026-08-10 13:10:12 +05:30
committed by GitHub
parent 044737a3a7
commit 19c6526841
9 changed files with 91 additions and 21 deletions
@@ -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.api.room.toMatrixUser
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -58,6 +59,7 @@ class RoomMemberListPresenter(
override fun present(): RoomMemberListState { override fun present(): RoomMemberListState {
val searchQuery = rememberTextFieldState() val searchQuery = rememberTextFieldState()
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val roomInfo by room.roomInfoFlow.collectAsState()
val canInvite by room.permissionsAsState(false) { perms -> perms.canOwnUserInvite() } val canInvite by room.permissionsAsState(false) { perms -> perms.canOwnUserInvite() }
val roomModerationState = roomMembersModerationPresenter.present() val roomModerationState = roomMembersModerationPresenter.present()
@@ -72,13 +74,18 @@ class RoomMemberListPresenter(
var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS) } var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS) }
var roomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) } var roomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) }
var filteredRoomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) } var filteredRoomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) }
val activeCallParticipants = roomInfo.activeRoomCallParticipants
val comparator = remember(activeCallParticipants) {
roomMemberListComparator(activeCallParticipants)
}
// Update the room members when the screen is loaded // Update the room members when the screen is loaded
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
room.updateMembers() room.updateMembers()
} }
LaunchedEffect(membersState, roomMemberIdentityStates) { LaunchedEffect(membersState, roomMemberIdentityStates, activeCallParticipants) {
if (membersState is RoomMembersState.Unknown) { if (membersState is RoomMembersState.Unknown) {
return@LaunchedEffect return@LaunchedEffect
} }
@@ -90,23 +97,23 @@ class RoomMemberListPresenter(
} }
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
val members = membersState.roomMembers().orEmpty().groupBy { it.membership } val members = membersState.roomMembers().orEmpty().groupBy { it.membership }
val info = room.info() if (members.getOrDefault(RoomMembershipState.JOIN, emptyList()).size < roomInfo.joinedMembersCount / 2) {
if (members.getOrDefault(RoomMembershipState.JOIN, emptyList()).size < info.joinedMembersCount / 2) {
// Don't display initial room member list if we have less than half of the joined members: // 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 return@withContext
} }
val result = RoomMembers( val result = RoomMembers(
invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()) invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList())
.map { it.withIdentityState(roomMemberIdentityStates) } .map { it.withIdentityState(roomMemberIdentityStates, activeCallParticipants) }
.toImmutableList(), .toImmutableList(),
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()) joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList())
.sortedWith(powerLevelRoomMemberComparator) .sortedWith(comparator)
.map { it.withIdentityState(roomMemberIdentityStates) } // .sortedWith(powerLevelRoomMemberComparator)
.map { it.withIdentityState(roomMemberIdentityStates, activeCallParticipants) }
.toImmutableList(), .toImmutableList(),
banned = members.getOrDefault(RoomMembershipState.BAN, emptyList()) banned = members.getOrDefault(RoomMembershipState.BAN, emptyList())
.sortedBy { it.userId.value } .sortedBy { it.userId.value }
.map { it.withIdentityState(roomMemberIdentityStates) } .map { it.withIdentityState(roomMemberIdentityStates, activeCallParticipants) }
.toImmutableList(), .toImmutableList(),
) )
roomMembers = if (membersState is RoomMembersState.Pending) { roomMembers = if (membersState is RoomMembersState.Pending) {
@@ -150,12 +157,29 @@ class RoomMemberListPresenter(
return state return state
} }
private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap<UserId, IdentityState>): RoomMemberWithIdentityState { private suspend fun RoomMember.withIdentityState(
identityStates: ImmutableMap<UserId, IdentityState>,
activeCallParticipants: ImmutableList<UserId>
): RoomMemberWithIdentityState {
return if (room.info().isEncrypted != true) { return if (room.info().isEncrypted != true) {
RoomMemberWithIdentityState(this, null) RoomMemberWithIdentityState(this, null, userId in activeCallParticipants)
} else { } else {
val identityState = identityStates[userId] ?: encryptionService.getUserIdentity(userId, fallbackToServer = false).getOrNull() val identityState = identityStates[userId] ?: encryptionService.getUserIdentity(userId, fallbackToServer = false).getOrNull()
RoomMemberWithIdentityState(this, identityState) RoomMemberWithIdentityState(this, identityState, userId in activeCallParticipants)
} }
} }
private fun roomMemberListComparator(
activeCallParticipants: List<UserId>,
): Comparator<RoomMember> =
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)
}
}
} }
@@ -66,4 +66,5 @@ data class RoomMembers(
data class RoomMemberWithIdentityState( data class RoomMemberWithIdentityState(
val roomMember: RoomMember, val roomMember: RoomMember,
val identityState: IdentityState?, val identityState: IdentityState?,
val isInCall: Boolean
) )
@@ -78,7 +78,7 @@ private fun aLoadedRoomMembers() = AsyncData.Success(
joined = persistentListOf( joined = persistentListOf(
anAlice().withIdentity(identityState = IdentityState.Verified), anAlice().withIdentity(identityState = IdentityState.Verified),
aBob().withIdentity(identityState = IdentityState.PinViolation), aBob().withIdentity(identityState = IdentityState.PinViolation),
aCarol().withIdentity(), aCarol().withIdentity(isInCall = true),
aDavid().withIdentity(), aDavid().withIdentity(),
anEve().withIdentity(identityState = IdentityState.VerificationViolation) 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) 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
)
@@ -338,6 +338,15 @@ private fun RoomMemberListItem(
color = ElementTheme.colors.textSecondary, 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
)
}
} }
} }
) )
@@ -191,6 +191,38 @@ class RoomMemberListPresenterTest {
awaitItem().eventSink(RoomMemberListEvent.RoomMemberSelected(anInvitedVictor())) 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( private fun createFakeJoinedRoom(