Pin user identity.
This commit is contained in:
parent
9b94edcfa3
commit
9d815d26b4
7 changed files with 69 additions and 22 deletions
|
|
@ -12,33 +12,35 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.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.room.MatrixRoom
|
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.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class IdentityChangeStatePresenter @Inject constructor(
|
class IdentityChangeStatePresenter @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
private val encryptionService: EncryptionService,
|
||||||
) : Presenter<IdentityChangeState> {
|
) : Presenter<IdentityChangeState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): IdentityChangeState {
|
override fun present(): IdentityChangeState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val roomMemberIdentityStateChange = remember {
|
val roomMemberIdentityStateChange = remember {
|
||||||
mutableStateOf(emptyList<RoomMemberIdentityStateChange>())
|
mutableStateOf(persistentListOf<RoomMemberIdentityStateChange>())
|
||||||
}
|
|
||||||
|
|
||||||
// Keep the ignored alert locally for now
|
|
||||||
val ignoredUserIdChange = rememberSaveable {
|
|
||||||
mutableStateOf(emptyList<UserId>())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
@ -47,39 +49,41 @@ class IdentityChangeStatePresenter @Inject constructor(
|
||||||
|
|
||||||
fun handleEvent(event: IdentityChangeEvent) {
|
fun handleEvent(event: IdentityChangeEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is IdentityChangeEvent.Submit -> {
|
is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId)
|
||||||
ignoredUserIdChange.value += event.userId
|
|
||||||
// TODO notify the SDK
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return IdentityChangeState(
|
return IdentityChangeState(
|
||||||
roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value
|
roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value,
|
||||||
.filter { it.roomMember.userId !in ignoredUserIdChange.value }
|
|
||||||
.toImmutableList(),
|
|
||||||
eventSink = ::handleEvent,
|
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 ->
|
combine(room.identityStateChangesFlow, room.membersStateFlow) { IdentityStateChanges, membersState ->
|
||||||
IdentityStateChanges.map { IdentityStateChange ->
|
IdentityStateChanges.map { identityStateChange ->
|
||||||
val member = membersState.roomMembers()
|
val member = membersState.roomMembers()
|
||||||
?.firstOrNull { roomMember -> roomMember.userId == IdentityStateChange.userId }
|
?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId }
|
||||||
?: createDefaultRoomMemberForIdentityChange(IdentityStateChange.userId)
|
?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId)
|
||||||
RoomMemberIdentityStateChange(
|
RoomMemberIdentityStateChange(
|
||||||
roomMember = member,
|
roomMember = member,
|
||||||
identityState = IdentityStateChange.identityState,
|
identityState = identityStateChange.identityState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { roomMemberIdentityStateChanges ->
|
.onEach { roomMemberIdentityStateChanges ->
|
||||||
roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges
|
roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges.toPersistentList()
|
||||||
}
|
}
|
||||||
.launchIn(this)
|
.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_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
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.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.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
|
@ -1019,6 +1020,7 @@ class MessagesPresenterTest {
|
||||||
val featureFlagService = FakeFeatureFlagService()
|
val featureFlagService = FakeFeatureFlagService()
|
||||||
val identityChangeStatePresenter = IdentityChangeStatePresenter(
|
val identityChangeStatePresenter = IdentityChangeStatePresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
|
encryptionService = FakeEncryptionService(),
|
||||||
)
|
)
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,19 @@ package io.element.android.features.messages.impl.crypto.identity
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.typing.aTypingRoomMember
|
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.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
|
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.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
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.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.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
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 io.element.android.tests.testutils.test
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.test.runTest
|
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(
|
private fun createIdentityChangeStatePresenter(
|
||||||
room: MatrixRoom = FakeMatrixRoom(),
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||||
): IdentityChangeStatePresenter {
|
): IdentityChangeStatePresenter {
|
||||||
return IdentityChangeStatePresenter(
|
return IdentityChangeStatePresenter(
|
||||||
room = room,
|
room = room,
|
||||||
|
encryptionService = encryptionService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.encryption
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.
|
* Starts the identity reset process. This will return a handle that can be used to reset the identity.
|
||||||
*/
|
*/
|
||||||
suspend fun startIdentityReset(): Result<IdentityResetHandle?>
|
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.flatMap
|
||||||
import io.element.android.libraries.core.extensions.mapFailure
|
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.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.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
||||||
|
|
@ -202,4 +203,9 @@ internal class RustEncryptionService(
|
||||||
RustIdentityResetHandleFactory.create(sessionId, handle)
|
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
|
package io.element.android.libraries.matrix.impl.mapper
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
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) {
|
fun RustIdentityState.map(): IdentityState = when (this) {
|
||||||
RustIdentityState.VERIFIED -> IdentityState.Verified
|
RustIdentityState.VERIFIED -> IdentityState.Verified
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.test.encryption
|
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.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
||||||
|
|
@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
class FakeEncryptionService(
|
class FakeEncryptionService(
|
||||||
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
|
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
|
||||||
|
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
|
||||||
) : EncryptionService {
|
) : EncryptionService {
|
||||||
private var disableRecoveryFailure: Exception? = null
|
private var disableRecoveryFailure: Exception? = null
|
||||||
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
|
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
|
||||||
|
|
@ -117,6 +119,10 @@ class FakeEncryptionService(
|
||||||
return startIdentityResetLambda()
|
return startIdentityResetLambda()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun pinUserIdentity(userId: UserId): Result<Unit> {
|
||||||
|
return pinUserIdentityResult(userId)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val FAKE_RECOVERY_KEY = "fake"
|
const val FAKE_RECOVERY_KEY = "fake"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue