mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-05 12:54:35 +00:00
+35
-11
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-2
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
+9
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+32
@@ -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(
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user