feature(crypto): verification violation handling and block sending
This commit is contained in:
parent
79293e1e9d
commit
1087b151a3
13 changed files with 224 additions and 14 deletions
|
|
@ -10,5 +10,6 @@ package io.element.android.features.messages.impl.crypto.identity
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
sealed interface IdentityChangeEvent {
|
sealed interface IdentityChangeEvent {
|
||||||
data class Submit(val userId: UserId) : IdentityChangeEvent
|
data class PinViolation(val userId: UserId) : IdentityChangeEvent
|
||||||
|
data class VerificationViolation(val userId: UserId) : IdentityChangeEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,12 @@ class IdentityChangeStatePresenter @Inject constructor(
|
||||||
|
|
||||||
fun handleEvent(event: IdentityChangeEvent) {
|
fun handleEvent(event: IdentityChangeEvent) {
|
||||||
when (event) {
|
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")
|
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,
|
userId = userId,
|
||||||
displayNameOrDefault = displayNameOrDefault,
|
displayNameOrDefault = displayNameOrDefault,
|
||||||
avatarData = getAvatarData(AvatarSize.ComposerAlert),
|
avatarData = getAvatarData(AvatarSize.ComposerAlert),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
|
fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayNameOrDefault = userId.extractedDisplayName,
|
displayNameOrDefault = userId.extractedDisplayName,
|
||||||
avatarData = AvatarData(
|
avatarData = AvatarData(
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState
|
||||||
roomMemberIdentityStateChanges = listOf(
|
roomMemberIdentityStateChanges = listOf(
|
||||||
aRoomMemberIdentityStateChange(
|
aRoomMemberIdentityStateChange(
|
||||||
identityRoomMember = anIdentityRoomMember(displayNameOrDefault = "Alice"),
|
identityRoomMember = anIdentityRoomMember(displayNameOrDefault = "Alice"),
|
||||||
identityState = IdentityState.PinViolation,
|
identityState = IdentityState.VerificationViolation,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.appconfig.LearnMoreConfig
|
import io.element.android.appconfig.LearnMoreConfig
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -32,7 +33,8 @@ fun IdentityChangeStateView(
|
||||||
// Pick the first identity change to PinViolation
|
// Pick the first identity change to PinViolation
|
||||||
val pinViolationIdentityChange = state.roomMemberIdentityStateChanges.firstOrNull {
|
val pinViolationIdentityChange = state.roomMemberIdentityStateChanges.firstOrNull {
|
||||||
// For now only render PinViolation
|
// For now only render PinViolation
|
||||||
it.identityState == IdentityState.PinViolation
|
it.identityState == IdentityState.PinViolation ||
|
||||||
|
it.identityState == IdentityState.VerificationViolation
|
||||||
}
|
}
|
||||||
if (pinViolationIdentityChange != null) {
|
if (pinViolationIdentityChange != null) {
|
||||||
ComposerAlertMolecule(
|
ComposerAlertMolecule(
|
||||||
|
|
@ -45,12 +47,22 @@ fun IdentityChangeStateView(
|
||||||
CommonStrings.crypto_identity_change_pin_violation_new_user_id,
|
CommonStrings.crypto_identity_change_pin_violation_new_user_id,
|
||||||
pinViolationIdentityChange.identityRoomMember.userId,
|
pinViolationIdentityChange.identityRoomMember.userId,
|
||||||
)
|
)
|
||||||
val fullText = stringResource(
|
|
||||||
id = CommonStrings.crypto_identity_change_pin_violation_new,
|
val fullText = if (pinViolationIdentityChange.identityState == IdentityState.PinViolation) {
|
||||||
displayName,
|
stringResource(
|
||||||
userIdStr,
|
id = CommonStrings.crypto_identity_change_pin_violation_new,
|
||||||
learnMoreStr,
|
displayName,
|
||||||
)
|
userIdStr,
|
||||||
|
learnMoreStr,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
|
id = CommonStrings.crypto_identity_change_verification_violation_new,
|
||||||
|
displayName,
|
||||||
|
userIdStr,
|
||||||
|
learnMoreStr,
|
||||||
|
)
|
||||||
|
}
|
||||||
append(fullText)
|
append(fullText)
|
||||||
val userIdStartIndex = fullText.indexOf(userIdStr)
|
val userIdStartIndex = fullText.indexOf(userIdStr)
|
||||||
addStyle(
|
addStyle(
|
||||||
|
|
@ -65,6 +77,7 @@ fun IdentityChangeStateView(
|
||||||
style = SpanStyle(
|
style = SpanStyle(
|
||||||
textDecoration = TextDecoration.Underline,
|
textDecoration = TextDecoration.Underline,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = ElementTheme.colors.textPrimary
|
||||||
),
|
),
|
||||||
start = learnMoreStartIndex,
|
start = learnMoreStartIndex,
|
||||||
end = learnMoreStartIndex + learnMoreStr.length,
|
end = learnMoreStartIndex + learnMoreStr.length,
|
||||||
|
|
@ -80,7 +93,18 @@ fun IdentityChangeStateView(
|
||||||
end = learnMoreStartIndex + learnMoreStr.length,
|
end = learnMoreStartIndex + learnMoreStr.length,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSubmitClick = { state.eventSink(IdentityChangeEvent.Submit(pinViolationIdentityChange.identityRoomMember.userId)) },
|
submitText = if (pinViolationIdentityChange.identityState == IdentityState.VerificationViolation) {
|
||||||
|
stringResource(CommonStrings.crypto_identity_change_withdraw_verification_action)
|
||||||
|
} else {
|
||||||
|
stringResource(CommonStrings.action_ok)
|
||||||
|
},
|
||||||
|
onSubmitClick = {
|
||||||
|
if (pinViolationIdentityChange.identityState == IdentityState.VerificationViolation) {
|
||||||
|
state.eventSink(IdentityChangeEvent.VerificationViolation(pinViolationIdentityChange.identityRoomMember.userId))
|
||||||
|
} else {
|
||||||
|
state.eventSink(IdentityChangeEvent.PinViolation(pinViolationIdentityChange.identityRoomMember.userId))
|
||||||
|
}
|
||||||
|
},
|
||||||
isCritical = pinViolationIdentityChange.identityState == IdentityState.VerificationViolation,
|
isCritical = pinViolationIdentityChange.identityState == IdentityState.VerificationViolation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredHeightIn
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
import io.element.android.libraries.textcomposer.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DisabledComposerView(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.padding(4.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp),
|
||||||
|
enabled = false,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
resourceId = CommonDrawables.ic_plus_composer,
|
||||||
|
contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment),
|
||||||
|
tint = ElementTheme.colors.iconDisabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val bgColor = ElementTheme.colors.bgCanvasDisabled
|
||||||
|
val borderColor = ElementTheme.colors.borderDisabled
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(21.dp))
|
||||||
|
.border(0.5.dp, borderColor, RoundedCornerShape(21.dp))
|
||||||
|
.background(color = bgColor)
|
||||||
|
.size(42.dp)
|
||||||
|
.requiredHeightIn(min = 42.dp)
|
||||||
|
.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp),
|
||||||
|
enabled = false,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
imageVector = CompoundIcons.SendSolid(),
|
||||||
|
contentDescription = "",
|
||||||
|
tint = ElementTheme.colors.iconQuaternary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun DisabledMessageComposerViewVoicePreview() = ElementPreview {
|
||||||
|
Column {
|
||||||
|
DisabledComposerView(
|
||||||
|
modifier = Modifier.height(IntrinsicSize.Min),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,10 +14,12 @@ import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.ProduceStateScope
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
|
@ -33,6 +35,9 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.features.messages.impl.MessagesNavigator
|
import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||||
|
import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange
|
||||||
|
import io.element.android.features.messages.impl.crypto.identity.createDefaultRoomMemberForIdentityChange
|
||||||
|
import io.element.android.features.messages.impl.crypto.identity.toIdentityRoomMember
|
||||||
import io.element.android.features.messages.impl.draft.ComposerDraftService
|
import io.element.android.features.messages.impl.draft.ComposerDraftService
|
||||||
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource
|
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource
|
||||||
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor
|
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor
|
||||||
|
|
@ -52,6 +57,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||||
|
|
@ -76,18 +82,24 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||||
import io.element.android.wysiwyg.display.TextDisplay
|
import io.element.android.wysiwyg.display.TextDisplay
|
||||||
|
import kotlinx.collections.immutable.PersistentList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
@ -389,6 +401,11 @@ class MessageComposerPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val roomMemberIdentityStateChange by produceState(persistentListOf()) {
|
||||||
|
observeRoomMemberIdentityStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
return MessageComposerState(
|
return MessageComposerState(
|
||||||
textEditorState = textEditorState,
|
textEditorState = textEditorState,
|
||||||
isFullScreen = isFullScreen.value,
|
isFullScreen = isFullScreen.value,
|
||||||
|
|
@ -398,6 +415,7 @@ class MessageComposerPresenter @AssistedInject constructor(
|
||||||
canShareLocation = canShareLocation.value,
|
canShareLocation = canShareLocation.value,
|
||||||
canCreatePoll = canCreatePoll.value,
|
canCreatePoll = canCreatePoll.value,
|
||||||
suggestions = suggestions.toPersistentList(),
|
suggestions = suggestions.toPersistentList(),
|
||||||
|
roomMemberIdentityStateChanges = roomMemberIdentityStateChange,
|
||||||
resolveMentionDisplay = resolveMentionDisplay,
|
resolveMentionDisplay = resolveMentionDisplay,
|
||||||
eventSink = { handleEvents(it) },
|
eventSink = { handleEvents(it) },
|
||||||
)
|
)
|
||||||
|
|
@ -705,4 +723,33 @@ class MessageComposerPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.messages.impl.messagecomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
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.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
|
|
@ -24,6 +25,7 @@ data class MessageComposerState(
|
||||||
val canShareLocation: Boolean,
|
val canShareLocation: Boolean,
|
||||||
val canCreatePoll: Boolean,
|
val canCreatePoll: Boolean,
|
||||||
val suggestions: ImmutableList<ResolvedSuggestion>,
|
val suggestions: ImmutableList<ResolvedSuggestion>,
|
||||||
|
val roomMemberIdentityStateChanges: ImmutableList<RoomMemberIdentityStateChange>,
|
||||||
val resolveMentionDisplay: (String, String) -> TextDisplay,
|
val resolveMentionDisplay: (String, String) -> TextDisplay,
|
||||||
val eventSink: (MessageComposerEvents) -> Unit,
|
val eventSink: (MessageComposerEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.messages.impl.messagecomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
|
|
@ -32,6 +33,7 @@ fun aMessageComposerState(
|
||||||
canShareLocation: Boolean = true,
|
canShareLocation: Boolean = true,
|
||||||
canCreatePoll: Boolean = true,
|
canCreatePoll: Boolean = true,
|
||||||
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
|
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
|
||||||
|
identityStates: ImmutableList<RoomMemberIdentityStateChange> = persistentListOf(),
|
||||||
eventSink: (MessageComposerEvents) -> Unit = {},
|
eventSink: (MessageComposerEvents) -> Unit = {},
|
||||||
) = MessageComposerState(
|
) = MessageComposerState(
|
||||||
textEditorState = textEditorState,
|
textEditorState = textEditorState,
|
||||||
|
|
@ -42,6 +44,7 @@ fun aMessageComposerState(
|
||||||
canShareLocation = canShareLocation,
|
canShareLocation = canShareLocation,
|
||||||
canCreatePoll = canCreatePoll,
|
canCreatePoll = canCreatePoll,
|
||||||
suggestions = suggestions,
|
suggestions = suggestions,
|
||||||
|
roomMemberIdentityStateChanges = identityStates,
|
||||||
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
|
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.TextComposer
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||||
|
|
@ -37,6 +38,14 @@ internal fun MessageComposerView(
|
||||||
enableVoiceMessages: Boolean,
|
enableVoiceMessages: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val verificationViolation = state.roomMemberIdentityStateChanges.firstOrNull {
|
||||||
|
it.identityState == IdentityState.VerificationViolation
|
||||||
|
}
|
||||||
|
if (verificationViolation != null) {
|
||||||
|
DisabledComposerView(modifier = modifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
fun sendMessage() {
|
fun sendMessage() {
|
||||||
state.eventSink(MessageComposerEvents.SendMessage)
|
state.eventSink(MessageComposerEvents.SendMessage)
|
||||||
|
|
@ -139,6 +148,7 @@ internal fun MessageComposerViewPreview(
|
||||||
enableVoiceMessages = true,
|
enableVoiceMessages = true,
|
||||||
subcomposing = false,
|
subcomposing = false,
|
||||||
)
|
)
|
||||||
|
DisabledComposerView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,11 @@ fun ComposerAlertMolecule(
|
||||||
text = content,
|
text = content,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = if (isCritical) {
|
||||||
|
ElementTheme.colors.textCriticalPrimary
|
||||||
|
} else {
|
||||||
|
ElementTheme.colors.textPrimary
|
||||||
|
},
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ interface EncryptionService {
|
||||||
* Remember this identity, ensuring it does not result in a pin violation.
|
* Remember this identity, ensuring it does not result in a pin violation.
|
||||||
*/
|
*/
|
||||||
suspend fun pinUserIdentity(userId: UserId): Result<Unit>
|
suspend fun pinUserIdentity(userId: UserId): Result<Unit>
|
||||||
|
suspend fun withdrawVerificationRequirement(userId: UserId): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,10 @@ internal class RustEncryptionService(
|
||||||
getUserIdentity(userId).pin()
|
getUserIdentity(userId).pin()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun withdrawVerificationRequirement(userId: UserId): Result<Unit> = runCatching {
|
||||||
|
getUserIdentity(userId).withdrawVerification()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getUserIdentity(userId: UserId): UserIdentity {
|
private suspend fun getUserIdentity(userId: UserId): UserIdentity {
|
||||||
return service.userIdentity(
|
return service.userIdentity(
|
||||||
userId = userId.value,
|
userId = userId.value,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class FakeEncryptionService(
|
||||||
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
|
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
|
||||||
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
|
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
|
||||||
private val isUserVerifiedResult: (UserId) -> Result<Boolean> = { lambdaError() },
|
private val isUserVerifiedResult: (UserId) -> Result<Boolean> = { lambdaError() },
|
||||||
|
private val withdrawVerificationResult: (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)
|
||||||
|
|
@ -124,6 +125,10 @@ class FakeEncryptionService(
|
||||||
return pinUserIdentityResult(userId)
|
return pinUserIdentityResult(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun withdrawVerificationRequirement(userId: UserId): Result<Unit> {
|
||||||
|
return withdrawVerificationResult(userId)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun isUserVerified(userId: UserId): Result<Boolean> = simulateLongTask {
|
override suspend fun isUserVerified(userId: UserId): Result<Boolean> = simulateLongTask {
|
||||||
isUserVerifiedResult(userId)
|
isUserVerifiedResult(userId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue