[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.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<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
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<UserId, IdentityState>): RoomMemberWithIdentityState {
private suspend fun RoomMember.withIdentityState(
identityStates: ImmutableMap<UserId, IdentityState>,
activeCallParticipants: ImmutableList<UserId>
): 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<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(
val roomMember: RoomMember,
val identityState: IdentityState?,
val isInCall: Boolean
)
@@ -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
)
@@ -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
)
}
}
}
)
@@ -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(