diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt index e74004c1d0..bc11f29df3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt @@ -10,5 +10,6 @@ package io.element.android.features.messages.impl.crypto.identity import io.element.android.libraries.matrix.api.core.UserId sealed interface IdentityChangeEvent { - data class Submit(val userId: UserId) : IdentityChangeEvent + data class PinViolation(val userId: UserId) : IdentityChangeEvent + data class VerificationViolation(val userId: UserId) : IdentityChangeEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index adc39fbc74..2b41091168 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -49,7 +49,12 @@ class IdentityChangeStatePresenter @Inject constructor( fun handleEvent(event: IdentityChangeEvent) { when (event) { - is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId) + is IdentityChangeEvent.VerificationViolation -> { + coroutineScope.withdrawVerificationRequirement(event.userId) + } + is IdentityChangeEvent.PinViolation -> { + coroutineScope.pinUserIdentity(event.userId) + } } } @@ -94,15 +99,22 @@ class IdentityChangeStatePresenter @Inject constructor( Timber.e(it, "Failed to pin identity for user $userId") } } + + private fun CoroutineScope.withdrawVerificationRequirement(userId: UserId) = launch { + encryptionService.withdrawVerificationRequirement(userId) + .onFailure { + Timber.e(it, "Failed to withdraw verification for user $userId") + } + } } -private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember( +fun RoomMember.toIdentityRoomMember() = IdentityRoomMember( userId = userId, displayNameOrDefault = displayNameOrDefault, avatarData = getAvatarData(AvatarSize.ComposerAlert), ) -private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember( +fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember( userId = userId, displayNameOrDefault = userId.extractedDisplayName, avatarData = AvatarData( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt index 5ec3858809..d381d17de9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt @@ -30,7 +30,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider>.observeRoomMemberIdentityStateChange() { + room.syncUpdateFlow + .filter { + // Room cannot become unencrypted, so we can just apply a filter here. + room.isEncrypted + } + .distinctUntilChanged() + .flatMapLatest { + combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState -> + identityStateChanges.map { identityStateChange -> + val member = membersState.roomMembers() + ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } + ?.toIdentityRoomMember() + ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId) + RoomMemberIdentityStateChange( + identityRoomMember = member, + identityState = identityStateChange.identityState, + ) + } + } + .distinctUntilChanged() + .onEach { roomMemberIdentityStateChanges -> + value = roomMemberIdentityStateChanges.toPersistentList() + } + } + .launchIn(this) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index d86ba8d98c..4bb0f7f6fd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Stable +import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.TextEditorState @@ -24,6 +25,7 @@ data class MessageComposerState( val canShareLocation: Boolean, val canCreatePoll: Boolean, val suggestions: ImmutableList, + val roomMemberIdentityStateChanges: ImmutableList, val resolveMentionDisplay: (String, String) -> TextDisplay, val eventSink: (MessageComposerEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index c0ea895618..4e0a6df5c9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.TextEditorState @@ -32,6 +33,7 @@ fun aMessageComposerState( canShareLocation: Boolean = true, canCreatePoll: Boolean = true, suggestions: ImmutableList = persistentListOf(), + identityStates: ImmutableList = persistentListOf(), eventSink: (MessageComposerEvents) -> Unit = {}, ) = MessageComposerState( textEditorState = textEditorState, @@ -42,6 +44,7 @@ fun aMessageComposerState( canShareLocation = canShareLocation, canCreatePoll = canCreatePoll, suggestions = suggestions, + roomMemberIdentityStateChanges = identityStates, resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 86e30a22c0..09289305b2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.textcomposer.TextComposer import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent @@ -37,6 +38,14 @@ internal fun MessageComposerView( enableVoiceMessages: Boolean, modifier: Modifier = Modifier, ) { + val verificationViolation = state.roomMemberIdentityStateChanges.firstOrNull { + it.identityState == IdentityState.VerificationViolation + } + if (verificationViolation != null) { + DisabledComposerView(modifier = modifier) + return + } + val view = LocalView.current fun sendMessage() { state.eventSink(MessageComposerEvents.SendMessage) @@ -139,6 +148,7 @@ internal fun MessageComposerViewPreview( enableVoiceMessages = true, subcomposing = false, ) + DisabledComposerView() } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt index 30944ead69..b85e31c514 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt @@ -78,7 +78,11 @@ fun ComposerAlertMolecule( text = content, modifier = Modifier.weight(1f), style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textPrimary, + color = if (isCritical) { + ElementTheme.colors.textCriticalPrimary + } else { + ElementTheme.colors.textPrimary + }, textAlign = TextAlign.Start, ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index bc009d3ee8..5b2703bf25 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -66,6 +66,7 @@ interface EncryptionService { * Remember this identity, ensuring it does not result in a pin violation. */ suspend fun pinUserIdentity(userId: UserId): Result + suspend fun withdrawVerificationRequirement(userId: UserId): Result } /** diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index 3e0e6571d4..3c3228ceb9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -209,6 +209,10 @@ internal class RustEncryptionService( getUserIdentity(userId).pin() } + override suspend fun withdrawVerificationRequirement(userId: UserId): Result = runCatching { + getUserIdentity(userId).withdrawVerification() + } + private suspend fun getUserIdentity(userId: UserId): UserIdentity { return service.userIdentity( userId = userId.value, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index ee126e2f92..0be0434d38 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -24,6 +24,7 @@ class FakeEncryptionService( var startIdentityResetLambda: () -> Result = { lambdaError() }, private val pinUserIdentityResult: (UserId) -> Result = { lambdaError() }, private val isUserVerifiedResult: (UserId) -> Result = { lambdaError() }, + private val withdrawVerificationResult: (UserId) -> Result = { lambdaError() }, ) : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) @@ -124,6 +125,10 @@ class FakeEncryptionService( return pinUserIdentityResult(userId) } + override suspend fun withdrawVerificationRequirement(userId: UserId): Result { + return withdrawVerificationResult(userId) + } + override suspend fun isUserVerified(userId: UserId): Result = simulateLongTask { isUserVerifiedResult(userId) }