Merge pull request #4824 from element-hq/feature/fga/fix_identity_change
fix (identity change) : RoomMemberIdentityStateChange in non encrypted room
This commit is contained in:
commit
45113818eb
6 changed files with 437 additions and 45 deletions
|
|
@ -15,7 +15,7 @@ import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.ui.room.observeRoomMemberIdentityStateChange
|
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -30,7 +30,7 @@ class IdentityChangeStatePresenter @Inject constructor(
|
||||||
override fun present(): IdentityChangeState {
|
override fun present(): IdentityChangeState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val roomMemberIdentityStateChange by produceState(persistentListOf()) {
|
val roomMemberIdentityStateChange by produceState(persistentListOf()) {
|
||||||
observeRoomMemberIdentityStateChange(room)
|
room.roomMemberIdentityStateChange(waitForEncryption = true).collect { value = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: IdentityChangeEvent) {
|
fun handleEvent(event: IdentityChangeEvent) {
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasMemberVerificationViolations by produceState(false) {
|
val hasMemberVerificationViolations by produceState(false) {
|
||||||
room.roomMemberIdentityStateChange()
|
room.roomMemberIdentityStateChange(waitForEncryption = true)
|
||||||
.onEach { identities -> value = identities.any { it.identityState == IdentityState.VerificationViolation } }
|
.onEach { identities -> value = identities.any { it.identityState == IdentityState.VerificationViolation } }
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
val roomModerationState = roomMembersModerationPresenter.present()
|
val roomModerationState = roomMembersModerationPresenter.present()
|
||||||
|
|
||||||
val roomMemberIdentityStates by produceState(persistentMapOf<UserId, IdentityState>()) {
|
val roomMemberIdentityStates by produceState(persistentMapOf<UserId, IdentityState>()) {
|
||||||
room.roomMemberIdentityStateChange()
|
room.roomMemberIdentityStateChange(waitForEncryption = true)
|
||||||
.onEach { identities ->
|
.onEach { identities ->
|
||||||
value = identities.associateBy({ it.identityRoomMember.userId }, { it.identityState }).toPersistentMap()
|
value = identities.associateBy({ it.identityRoomMember.userId }, { it.identityState }).toPersistentMap()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl.members.details
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -33,9 +34,7 @@ import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
|
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -86,31 +85,30 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
val userProfileState = userProfilePresenter.present()
|
val userProfileState = userProfilePresenter.present()
|
||||||
|
|
||||||
val identityStateChanges by produceState<IdentityStateChange?>(initialValue = null) {
|
val identityStateChanges = produceState<IdentityStateChange?>(initialValue = null) {
|
||||||
room.roomInfoFlow.filter { it.isEncrypted == true }
|
// Fetch the initial identity state manually
|
||||||
.flatMapLatest {
|
val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull()
|
||||||
// Fetch the initial identity state manually
|
value = identityState?.let { IdentityStateChange(roomMemberId, it) }
|
||||||
val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull()
|
|
||||||
value = identityState?.let { IdentityStateChange(roomMemberId, it) }
|
|
||||||
|
|
||||||
// Subscribe to the identity changes
|
// Subscribe to the identity changes
|
||||||
room.roomMemberIdentityStateChange()
|
room.roomMemberIdentityStateChange(waitForEncryption = false)
|
||||||
.map { it.find { it.identityRoomMember.userId == roomMemberId } }
|
.map { it.find { it.identityRoomMember.userId == roomMemberId } }
|
||||||
.map { roomMemberIdentityStateChange ->
|
.map { roomMemberIdentityStateChange ->
|
||||||
// If we didn't receive any info, manually fetch it
|
// If we didn't receive any info, manually fetch it
|
||||||
roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull()
|
roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull()
|
||||||
}
|
|
||||||
.filterNotNull()
|
|
||||||
}
|
}
|
||||||
|
.filterNotNull()
|
||||||
.collect { value = IdentityStateChange(roomMemberId, it) }
|
.collect { value = IdentityStateChange(roomMemberId, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val verificationState = remember(identityStateChanges) {
|
val verificationState by remember {
|
||||||
when (identityStateChanges?.identityState) {
|
derivedStateOf {
|
||||||
IdentityState.VerificationViolation -> UserProfileVerificationState.VERIFICATION_VIOLATION
|
when (identityStateChanges.value?.identityState) {
|
||||||
IdentityState.Verified -> UserProfileVerificationState.VERIFIED
|
IdentityState.VerificationViolation -> UserProfileVerificationState.VERIFICATION_VIOLATION
|
||||||
IdentityState.Pinned, IdentityState.PinViolation -> UserProfileVerificationState.UNVERIFIED
|
IdentityState.Verified -> UserProfileVerificationState.VERIFIED
|
||||||
else -> UserProfileVerificationState.UNKNOWN
|
IdentityState.Pinned, IdentityState.PinViolation -> UserProfileVerificationState.UNVERIFIED
|
||||||
|
else -> UserProfileVerificationState.UNKNOWN
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.ui.room
|
package io.element.android.libraries.matrix.ui.room
|
||||||
|
|
||||||
import androidx.compose.runtime.ProduceStateScope
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -17,25 +16,25 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.PersistentList
|
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun JoinedRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIdentityStateChange>> {
|
fun JoinedRoom.roomMemberIdentityStateChange(waitForEncryption: Boolean): Flow<ImmutableList<RoomMemberIdentityStateChange>> {
|
||||||
return roomInfoFlow
|
val encryptionChangeFlow = flow {
|
||||||
.filter {
|
if (waitForEncryption) {
|
||||||
// Room cannot become unencrypted, so we can just apply a filter here.
|
// Room cannot become unencrypted, so it's ok to use first here
|
||||||
it.isEncrypted == true
|
roomInfoFlow.first { roomInfo -> roomInfo.isEncrypted == true }
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
emit(Unit)
|
||||||
|
}
|
||||||
|
return encryptionChangeFlow
|
||||||
.flatMapLatest {
|
.flatMapLatest {
|
||||||
combine(identityStateChangesFlow, membersStateFlow) { identityStateChanges, membersState ->
|
combine(identityStateChangesFlow, membersStateFlow) { identityStateChanges, membersState ->
|
||||||
identityStateChanges.map { identityStateChange ->
|
identityStateChanges.map { identityStateChange ->
|
||||||
|
|
@ -52,14 +51,6 @@ fun JoinedRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIde
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange(room: JoinedRoom) {
|
|
||||||
room.roomMemberIdentityStateChange()
|
|
||||||
.onEach { roomMemberIdentityStateChanges ->
|
|
||||||
value = roomMemberIdentityStateChanges.toPersistentList()
|
|
||||||
}
|
|
||||||
.launchIn(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
|
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayNameOrDefault = displayNameOrDefault,
|
displayNameOrDefault = displayNameOrDefault,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,403 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.ui.room
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ObserveRoomMemberIdentityStateChangeTest {
|
||||||
|
private val aliceRoomMember = aRoomMember(A_USER_ID, displayName = "Alice")
|
||||||
|
private val bobRoomMember = aRoomMember(A_USER_ID_2, displayName = "Bob")
|
||||||
|
private val carolRoomMember = aRoomMember(A_USER_ID_3, displayName = "Carol")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange emits empty list for non-encrypted room with no identity changes`() =
|
||||||
|
runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow<List<IdentityStateChange>>(emptyList())
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = false))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test {
|
||||||
|
val result = awaitItem()
|
||||||
|
assertThat(result).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange emits identity changes for non-encrypted room when waitForEncryption is false`() =
|
||||||
|
runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow(
|
||||||
|
listOf(
|
||||||
|
IdentityStateChange(bobRoomMember.userId, IdentityState.Verified),
|
||||||
|
IdentityStateChange(carolRoomMember.userId, IdentityState.PinViolation)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = false))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember,
|
||||||
|
carolRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test {
|
||||||
|
val result = awaitItem()
|
||||||
|
assertThat(result).hasSize(2)
|
||||||
|
|
||||||
|
val bobChange = result.find { it.identityRoomMember.userId == bobRoomMember.userId }
|
||||||
|
assertThat(bobChange).isNotNull()
|
||||||
|
assertThat(bobChange?.identityState).isEqualTo(IdentityState.Verified)
|
||||||
|
assertThat(bobChange?.identityRoomMember?.displayNameOrDefault).isEqualTo("Bob")
|
||||||
|
|
||||||
|
val carolChange = result.find { it.identityRoomMember.userId == carolRoomMember.userId }
|
||||||
|
assertThat(carolChange).isNotNull()
|
||||||
|
assertThat(carolChange?.identityState).isEqualTo(IdentityState.PinViolation)
|
||||||
|
assertThat(carolChange?.identityRoomMember?.displayNameOrDefault).isEqualTo("Carol")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange emits identity changes for already encrypted room`() =
|
||||||
|
runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow(
|
||||||
|
listOf(
|
||||||
|
IdentityStateChange(bobRoomMember.userId, IdentityState.VerificationViolation)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test {
|
||||||
|
val result = awaitItem()
|
||||||
|
assertThat(result).hasSize(1)
|
||||||
|
|
||||||
|
val bobChange = result.first()
|
||||||
|
assertThat(bobChange.identityRoomMember.userId).isEqualTo(bobRoomMember.userId)
|
||||||
|
assertThat(bobChange.identityState).isEqualTo(IdentityState.VerificationViolation)
|
||||||
|
assertThat(bobChange.identityRoomMember.displayNameOrDefault).isEqualTo("Bob")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange waits for encryption before emitting when waitForEncryption is true`() =
|
||||||
|
runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow(
|
||||||
|
listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned))
|
||||||
|
)
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = false))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test {
|
||||||
|
// Should not emit anything yet since room is not encrypted
|
||||||
|
expectNoEvents()
|
||||||
|
|
||||||
|
// Enable encryption
|
||||||
|
joinedRoom.baseRoom.givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||||
|
|
||||||
|
val result = awaitItem()
|
||||||
|
assertThat(result).hasSize(1)
|
||||||
|
assertThat(result.first().identityRoomMember.userId).isEqualTo(bobRoomMember.userId)
|
||||||
|
assertThat(result.first().identityState).isEqualTo(IdentityState.Pinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange creates default member when room member not found`() =
|
||||||
|
runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow(
|
||||||
|
listOf(IdentityStateChange(carolRoomMember.userId, IdentityState.PinViolation))
|
||||||
|
)
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||||
|
// Only include aliceRoomMember and bobRoomMember, not carolRoomMember
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test {
|
||||||
|
val result = awaitItem()
|
||||||
|
assertThat(result).hasSize(1)
|
||||||
|
|
||||||
|
val carolChange = result.first()
|
||||||
|
assertThat(carolChange.identityRoomMember.userId).isEqualTo(carolRoomMember.userId)
|
||||||
|
assertThat(carolChange.identityState).isEqualTo(IdentityState.PinViolation)
|
||||||
|
// Should use extracted display name from user ID since member not found
|
||||||
|
assertThat(carolChange.identityRoomMember.displayNameOrDefault).isEqualTo(
|
||||||
|
carolRoomMember.userId.extractedDisplayName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange updates when identity state changes`() = runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow(
|
||||||
|
listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned))
|
||||||
|
)
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test {
|
||||||
|
val firstResult = awaitItem()
|
||||||
|
assertThat(firstResult).hasSize(1)
|
||||||
|
assertThat(firstResult.first().identityState).isEqualTo(IdentityState.Pinned)
|
||||||
|
|
||||||
|
// Update identity state
|
||||||
|
identityStateChangesFlow.value = listOf(
|
||||||
|
IdentityStateChange(bobRoomMember.userId, IdentityState.VerificationViolation)
|
||||||
|
)
|
||||||
|
|
||||||
|
val secondResult = awaitItem()
|
||||||
|
assertThat(secondResult).hasSize(1)
|
||||||
|
assertThat(secondResult.first().identityState).isEqualTo(IdentityState.VerificationViolation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange updates when members state changes`() = runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow(
|
||||||
|
listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Verified))
|
||||||
|
)
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test {
|
||||||
|
val firstResult = awaitItem()
|
||||||
|
assertThat(firstResult).hasSize(1)
|
||||||
|
assertThat(firstResult.first().identityRoomMember.displayNameOrDefault).isEqualTo("Bob")
|
||||||
|
|
||||||
|
// Update room member with different display name
|
||||||
|
val updatedMember2 = bobRoomMember.copy(displayName = "Bobby")
|
||||||
|
joinedRoom.baseRoom.givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(persistentListOf(aliceRoomMember, updatedMember2))
|
||||||
|
)
|
||||||
|
|
||||||
|
val secondResult = awaitItem()
|
||||||
|
assertThat(secondResult).hasSize(1)
|
||||||
|
assertThat(secondResult.first().identityRoomMember.displayNameOrDefault).isEqualTo("Bobby")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange handles multiple identity states`() = runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow(
|
||||||
|
listOf(
|
||||||
|
IdentityStateChange(aliceRoomMember.userId, IdentityState.Verified),
|
||||||
|
IdentityStateChange(bobRoomMember.userId, IdentityState.PinViolation),
|
||||||
|
IdentityStateChange(carolRoomMember.userId, IdentityState.VerificationViolation)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember,
|
||||||
|
carolRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test {
|
||||||
|
val result = awaitItem()
|
||||||
|
assertThat(result).hasSize(3)
|
||||||
|
|
||||||
|
val verifiedUser = result.find { it.identityState == IdentityState.Verified }
|
||||||
|
assertThat(verifiedUser?.identityRoomMember?.userId).isEqualTo(aliceRoomMember.userId)
|
||||||
|
|
||||||
|
val pinViolationUser = result.find { it.identityState == IdentityState.PinViolation }
|
||||||
|
assertThat(pinViolationUser?.identityRoomMember?.userId).isEqualTo(bobRoomMember.userId)
|
||||||
|
|
||||||
|
val verificationViolationUser =
|
||||||
|
result.find { it.identityState == IdentityState.VerificationViolation }
|
||||||
|
assertThat(verificationViolationUser?.identityRoomMember?.userId).isEqualTo(carolRoomMember.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange handles room becoming encrypted scenario`() = runTest {
|
||||||
|
val identityStateChangesFlow = MutableStateFlow(
|
||||||
|
listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned))
|
||||||
|
)
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = false))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test {
|
||||||
|
// Should not emit anything initially as room is not encrypted
|
||||||
|
expectNoEvents()
|
||||||
|
|
||||||
|
// Room becomes encrypted
|
||||||
|
joinedRoom.baseRoom.givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||||
|
|
||||||
|
val result = awaitItem()
|
||||||
|
assertThat(result).hasSize(1)
|
||||||
|
assertThat(result.first().identityRoomMember.userId).isEqualTo(bobRoomMember.userId)
|
||||||
|
assertThat(result.first().identityState).isEqualTo(IdentityState.Pinned)
|
||||||
|
|
||||||
|
// Add more identity changes after encryption is enabled
|
||||||
|
identityStateChangesFlow.value = listOf(
|
||||||
|
IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned),
|
||||||
|
IdentityStateChange(aliceRoomMember.userId, IdentityState.VerificationViolation)
|
||||||
|
)
|
||||||
|
|
||||||
|
val updatedResult = awaitItem()
|
||||||
|
assertThat(updatedResult).hasSize(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomMemberIdentityStateChange does not emit duplicates for same state`() = runTest {
|
||||||
|
val identityStateChangesFlow = MutableSharedFlow<List<IdentityStateChange>>()
|
||||||
|
val identityStateChanges = listOf(
|
||||||
|
IdentityStateChange(bobRoomMember.userId, IdentityState.Verified)
|
||||||
|
)
|
||||||
|
|
||||||
|
val joinedRoom = FakeJoinedRoom(
|
||||||
|
identityStateChangesFlow = identityStateChangesFlow,
|
||||||
|
baseRoom = FakeJoinedRoom().baseRoom.apply {
|
||||||
|
givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||||
|
givenRoomMembersState(
|
||||||
|
RoomMembersState.Ready(
|
||||||
|
persistentListOf(
|
||||||
|
aliceRoomMember,
|
||||||
|
bobRoomMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test {
|
||||||
|
identityStateChangesFlow.emit(identityStateChanges)
|
||||||
|
|
||||||
|
val firstResult = awaitItem()
|
||||||
|
assertThat(firstResult).hasSize(1)
|
||||||
|
|
||||||
|
// Emit the same state again
|
||||||
|
identityStateChangesFlow.emit(identityStateChanges)
|
||||||
|
|
||||||
|
// Should not emit a new item due to distinctUntilChanged
|
||||||
|
expectNoEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue