Pin user identity.
This commit is contained in:
committed by
Benoit Marty
parent
9b94edcfa3
commit
9d815d26b4
@@ -12,33 +12,35 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class IdentityChangeStatePresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val encryptionService: EncryptionService,
|
||||
) : Presenter<IdentityChangeState> {
|
||||
@Composable
|
||||
override fun present(): IdentityChangeState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomMemberIdentityStateChange = remember {
|
||||
mutableStateOf(emptyList<RoomMemberIdentityStateChange>())
|
||||
}
|
||||
|
||||
// Keep the ignored alert locally for now
|
||||
val ignoredUserIdChange = rememberSaveable {
|
||||
mutableStateOf(emptyList<UserId>())
|
||||
mutableStateOf(persistentListOf<RoomMemberIdentityStateChange>())
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -47,39 +49,41 @@ class IdentityChangeStatePresenter @Inject constructor(
|
||||
|
||||
fun handleEvent(event: IdentityChangeEvent) {
|
||||
when (event) {
|
||||
is IdentityChangeEvent.Submit -> {
|
||||
ignoredUserIdChange.value += event.userId
|
||||
// TODO notify the SDK
|
||||
}
|
||||
is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId)
|
||||
}
|
||||
}
|
||||
|
||||
return IdentityChangeState(
|
||||
roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value
|
||||
.filter { it.roomMember.userId !in ignoredUserIdChange.value }
|
||||
.toImmutableList(),
|
||||
roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState<List<RoomMemberIdentityStateChange>>) {
|
||||
private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState<PersistentList<RoomMemberIdentityStateChange>>) {
|
||||
combine(room.identityStateChangesFlow, room.membersStateFlow) { IdentityStateChanges, membersState ->
|
||||
IdentityStateChanges.map { IdentityStateChange ->
|
||||
IdentityStateChanges.map { identityStateChange ->
|
||||
val member = membersState.roomMembers()
|
||||
?.firstOrNull { roomMember -> roomMember.userId == IdentityStateChange.userId }
|
||||
?: createDefaultRoomMemberForIdentityChange(IdentityStateChange.userId)
|
||||
?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId }
|
||||
?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId)
|
||||
RoomMemberIdentityStateChange(
|
||||
roomMember = member,
|
||||
identityState = IdentityStateChange.identityState,
|
||||
identityState = identityStateChange.identityState,
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { roomMemberIdentityStateChanges ->
|
||||
roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges
|
||||
roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges.toPersistentList()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.pinUserIdentity(userId: UserId) = launch {
|
||||
encryptionService.pinUserIdentity(userId)
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to pin identity for user $userId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,6 +64,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
@@ -1019,6 +1020,7 @@ class MessagesPresenterTest {
|
||||
val featureFlagService = FakeFeatureFlagService()
|
||||
val identityChangeStatePresenter = IdentityChangeStatePresenter(
|
||||
room = matrixRoom,
|
||||
encryptionService = FakeEncryptionService(),
|
||||
)
|
||||
return MessagesPresenter(
|
||||
room = matrixRoom,
|
||||
|
||||
@@ -9,13 +9,19 @@ package io.element.android.features.messages.impl.crypto.identity
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.impl.typing.aTypingRoomMember
|
||||
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.identity.IdentityState
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
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.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -94,11 +100,28 @@ class IdentityChangeStatePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when the user pin the identity, the presenter invokes the encryption service api`() =
|
||||
runTest {
|
||||
val lambda = lambdaRecorder<UserId, Result<Unit>> { Result.success(Unit) }
|
||||
val encryptionService = FakeEncryptionService(
|
||||
pinUserIdentityResult = lambda,
|
||||
)
|
||||
val presenter = createIdentityChangeStatePresenter(encryptionService = encryptionService)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(IdentityChangeEvent.Submit(A_USER_ID))
|
||||
lambda.assertions().isCalledOnce().with(value(A_USER_ID))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createIdentityChangeStatePresenter(
|
||||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
): IdentityChangeStatePresenter {
|
||||
return IdentityChangeStatePresenter(
|
||||
room = room,
|
||||
encryptionService = encryptionService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.api.encryption
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -58,6 +59,11 @@ interface EncryptionService {
|
||||
* Starts the identity reset process. This will return a handle that can be used to reset the identity.
|
||||
*/
|
||||
suspend fun startIdentityReset(): Result<IdentityResetHandle?>
|
||||
|
||||
/**
|
||||
* Remember this identity, ensuring it does not result in a pin violation.
|
||||
*/
|
||||
suspend fun pinUserIdentity(userId: UserId): Result<Unit>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
||||
@@ -202,4 +203,9 @@ internal class RustEncryptionService(
|
||||
RustIdentityResetHandleFactory.create(sessionId, handle)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun pinUserIdentity(userId: UserId): Result<Unit> = runCatching {
|
||||
val userIdentity = service.getUserIdentity(userId.value)
|
||||
userIdentity.pin()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import org.matrix.rustcomponents.sdk.IdentityState as RustIdentityState
|
||||
import uniffi.matrix_sdk_crypto.IdentityState as RustIdentityState
|
||||
|
||||
fun RustIdentityState.map(): IdentityState = when (this) {
|
||||
RustIdentityState.VERIFIED -> IdentityState.Verified
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.test.encryption
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
||||
@@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class FakeEncryptionService(
|
||||
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
|
||||
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
|
||||
) : EncryptionService {
|
||||
private var disableRecoveryFailure: Exception? = null
|
||||
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
|
||||
@@ -117,6 +119,10 @@ class FakeEncryptionService(
|
||||
return startIdentityResetLambda()
|
||||
}
|
||||
|
||||
override suspend fun pinUserIdentity(userId: UserId): Result<Unit> {
|
||||
return pinUserIdentityResult(userId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FAKE_RECOVERY_KEY = "fake"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user