Skip to content

Commit e128eca

Browse files
authored
Merge pull request #4126 from element-hq/feature/valere/support_verification_violation_banner
feature(crypto): verification violation handling and block sending
2 parents d1fc963 + cc9c7b1 commit e128eca

File tree

29 files changed

+476
-143
lines changed

29 files changed

+476
-143
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
3333
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
3434
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
3535
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
36+
import io.element.android.features.messages.impl.messagecomposer.observeRoomMemberIdentityStateChange
3637
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
3738
import io.element.android.features.messages.impl.timeline.TimelineController
3839
import io.element.android.features.messages.impl.timeline.TimelineEvents
@@ -78,6 +79,7 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
7879
import io.element.android.libraries.textcomposer.model.MessageComposerMode
7980
import io.element.android.libraries.ui.strings.CommonStrings
8081
import io.element.android.services.analytics.api.AnalyticsService
82+
import kotlinx.collections.immutable.persistentListOf
8183
import kotlinx.collections.immutable.toPersistentList
8284
import kotlinx.coroutines.CoroutineScope
8385
import kotlinx.coroutines.launch
@@ -154,7 +156,9 @@ class MessagesPresenter @AssistedInject constructor(
154156
var hasDismissedInviteDialog by rememberSaveable {
155157
mutableStateOf(false)
156158
}
157-
159+
val roomMemberIdentityStateChanges by produceState(persistentListOf()) {
160+
observeRoomMemberIdentityStateChange(room)
161+
}
158162
LaunchedEffect(Unit) {
159163
// Remove the unread flag on entering but don't send read receipts
160164
// as those will be handled by the timeline.
@@ -211,6 +215,7 @@ class MessagesPresenter @AssistedInject constructor(
211215
roomAvatar = roomAvatar,
212216
heroes = heroes,
213217
composerState = composerState,
218+
roomMemberIdentityStateChanges = roomMemberIdentityStateChanges,
214219
userEventPermissions = userEventPermissions,
215220
voiceMessageComposerState = voiceMessageComposerState,
216221
timelineState = timelineState,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl
1010
import androidx.compose.runtime.Immutable
1111
import io.element.android.features.messages.impl.actionlist.ActionListState
1212
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
13+
import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange
1314
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
1415
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
1516
import io.element.android.features.messages.impl.timeline.TimelineState
@@ -33,6 +34,7 @@ data class MessagesState(
3334
val heroes: ImmutableList<AvatarData>,
3435
val userEventPermissions: UserEventPermissions,
3536
val composerState: MessageComposerState,
37+
val roomMemberIdentityStateChanges: ImmutableList<RoomMemberIdentityStateChange>,
3638
val voiceMessageComposerState: VoiceMessageComposerState,
3739
val timelineState: TimelineState,
3840
val timelineProtectionState: TimelineProtectionState,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
1111
import io.element.android.features.messages.impl.actionlist.ActionListState
1212
import io.element.android.features.messages.impl.actionlist.anActionListState
1313
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
14+
import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange
1415
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
1516
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
1617
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
@@ -41,6 +42,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
4142
import io.element.android.libraries.matrix.api.core.RoomId
4243
import io.element.android.libraries.textcomposer.model.MessageComposerMode
4344
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
45+
import kotlinx.collections.immutable.ImmutableList
4446
import kotlinx.collections.immutable.persistentListOf
4547
import kotlinx.collections.immutable.persistentSetOf
4648

@@ -92,6 +94,7 @@ fun aMessagesState(
9294
isFullScreen = false,
9395
mode = MessageComposerMode.Normal,
9496
),
97+
roomMemberIdentityStateChanges: ImmutableList<RoomMemberIdentityStateChange> = persistentListOf(),
9598
voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
9699
timelineState: TimelineState = aTimelineState(
97100
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
@@ -117,6 +120,7 @@ fun aMessagesState(
117120
heroes = persistentListOf(),
118121
userEventPermissions = userEventPermissions,
119122
composerState = composerState,
123+
roomMemberIdentityStateChanges = roomMemberIdentityStateChanges,
120124
voiceMessageComposerState = voiceMessageComposerState,
121125
timelineProtectionState = timelineProtectionState,
122126
identityChangeState = identityChangeState,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListView
5555
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
5656
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView
5757
import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet
58+
import io.element.android.features.messages.impl.messagecomposer.DisabledComposerView
5859
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
5960
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
6061
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsPickerView
@@ -97,6 +98,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
9798
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
9899
import io.element.android.libraries.matrix.api.core.EventId
99100
import io.element.android.libraries.matrix.api.core.UserId
101+
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
100102
import io.element.android.libraries.textcomposer.model.TextEditorState
101103
import io.element.android.libraries.ui.strings.CommonStrings
102104
import kotlinx.collections.immutable.ImmutableList
@@ -425,13 +427,20 @@ private fun MessagesViewComposerBottomSheetContents(
425427
onLinkClick = onLinkClick,
426428
)
427429
}
428-
MessageComposerView(
429-
state = state.composerState,
430-
voiceMessageState = state.voiceMessageComposerState,
431-
subcomposing = subcomposing,
432-
enableVoiceMessages = state.enableVoiceMessages,
433-
modifier = Modifier.fillMaxWidth(),
434-
)
430+
val verificationViolation = state.roomMemberIdentityStateChanges.firstOrNull {
431+
it.identityState == IdentityState.VerificationViolation
432+
}
433+
if (verificationViolation != null) {
434+
DisabledComposerView(modifier = Modifier.fillMaxWidth())
435+
} else {
436+
MessageComposerView(
437+
state = state.composerState,
438+
voiceMessageState = state.voiceMessageComposerState,
439+
subcomposing = subcomposing,
440+
enableVoiceMessages = state.enableVoiceMessages,
441+
modifier = Modifier.fillMaxWidth(),
442+
)
443+
}
435444
}
436445
} else {
437446
CantSendMessageBanner()

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ package io.element.android.features.messages.impl.crypto.identity
1010
import io.element.android.libraries.matrix.api.core.UserId
1111

1212
sealed interface IdentityChangeEvent {
13-
data class Submit(val userId: UserId) : IdentityChangeEvent
13+
data class PinIdentity(val userId: UserId) : IdentityChangeEvent
14+
data class WithdrawVerification(val userId: UserId) : IdentityChangeEvent
1415
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt

Lines changed: 15 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,16 @@
88
package io.element.android.features.messages.impl.crypto.identity
99

1010
import androidx.compose.runtime.Composable
11-
import androidx.compose.runtime.ProduceStateScope
1211
import androidx.compose.runtime.getValue
1312
import androidx.compose.runtime.produceState
1413
import androidx.compose.runtime.rememberCoroutineScope
14+
import io.element.android.features.messages.impl.messagecomposer.observeRoomMemberIdentityStateChange
1515
import io.element.android.libraries.architecture.Presenter
16-
import io.element.android.libraries.designsystem.components.avatar.AvatarData
17-
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
1816
import io.element.android.libraries.matrix.api.core.UserId
1917
import io.element.android.libraries.matrix.api.encryption.EncryptionService
2018
import io.element.android.libraries.matrix.api.room.MatrixRoom
21-
import io.element.android.libraries.matrix.api.room.RoomMember
22-
import io.element.android.libraries.matrix.api.room.roomMembers
23-
import io.element.android.libraries.matrix.ui.model.getAvatarData
24-
import kotlinx.collections.immutable.PersistentList
2519
import kotlinx.collections.immutable.persistentListOf
26-
import kotlinx.collections.immutable.toPersistentList
2720
import kotlinx.coroutines.CoroutineScope
28-
import kotlinx.coroutines.ExperimentalCoroutinesApi
29-
import kotlinx.coroutines.flow.combine
30-
import kotlinx.coroutines.flow.distinctUntilChanged
31-
import kotlinx.coroutines.flow.filter
32-
import kotlinx.coroutines.flow.flatMapLatest
33-
import kotlinx.coroutines.flow.launchIn
34-
import kotlinx.coroutines.flow.onEach
3521
import kotlinx.coroutines.launch
3622
import timber.log.Timber
3723
import javax.inject.Inject
@@ -44,12 +30,17 @@ class IdentityChangeStatePresenter @Inject constructor(
4430
override fun present(): IdentityChangeState {
4531
val coroutineScope = rememberCoroutineScope()
4632
val roomMemberIdentityStateChange by produceState(persistentListOf()) {
47-
observeRoomMemberIdentityStateChange()
33+
observeRoomMemberIdentityStateChange(room)
4834
}
4935

5036
fun handleEvent(event: IdentityChangeEvent) {
5137
when (event) {
52-
is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId)
38+
is IdentityChangeEvent.WithdrawVerification -> {
39+
coroutineScope.withdrawVerification(event.userId)
40+
}
41+
is IdentityChangeEvent.PinIdentity -> {
42+
coroutineScope.pinUserIdentity(event.userId)
43+
}
5344
}
5445
}
5546

@@ -59,56 +50,17 @@ class IdentityChangeStatePresenter @Inject constructor(
5950
)
6051
}
6152

62-
@OptIn(ExperimentalCoroutinesApi::class)
63-
private fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange() {
64-
room.syncUpdateFlow
65-
.filter {
66-
// Room cannot become unencrypted, so we can just apply a filter here.
67-
room.isEncrypted
68-
}
69-
.distinctUntilChanged()
70-
.flatMapLatest {
71-
combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState ->
72-
identityStateChanges.map { identityStateChange ->
73-
val member = membersState.roomMembers()
74-
?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId }
75-
?.toIdentityRoomMember()
76-
?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId)
77-
RoomMemberIdentityStateChange(
78-
identityRoomMember = member,
79-
identityState = identityStateChange.identityState,
80-
)
81-
}
82-
}
83-
.distinctUntilChanged()
84-
.onEach { roomMemberIdentityStateChanges ->
85-
value = roomMemberIdentityStateChanges.toPersistentList()
86-
}
87-
}
88-
.launchIn(this)
89-
}
90-
9153
private fun CoroutineScope.pinUserIdentity(userId: UserId) = launch {
9254
encryptionService.pinUserIdentity(userId)
9355
.onFailure {
9456
Timber.e(it, "Failed to pin identity for user $userId")
9557
}
9658
}
97-
}
98-
99-
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
100-
userId = userId,
101-
displayNameOrDefault = displayNameOrDefault,
102-
avatarData = getAvatarData(AvatarSize.ComposerAlert),
103-
)
10459

105-
private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
106-
userId = userId,
107-
displayNameOrDefault = userId.extractedDisplayName,
108-
avatarData = AvatarData(
109-
id = userId.value,
110-
name = null,
111-
url = null,
112-
size = AvatarSize.ComposerAlert,
113-
),
114-
)
60+
private fun CoroutineScope.withdrawVerification(userId: UserId) = launch {
61+
encryptionService.withdrawVerification(userId)
62+
.onFailure {
63+
Timber.e(it, "Failed to withdraw verification for user $userId")
64+
}
65+
}
66+
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState
3030
roomMemberIdentityStateChanges = listOf(
3131
aRoomMemberIdentityStateChange(
3232
identityRoomMember = anIdentityRoomMember(displayNameOrDefault = "Alice"),
33-
identityState = IdentityState.PinViolation,
33+
identityState = IdentityState.VerificationViolation,
3434
),
3535
),
3636
),
@@ -47,9 +47,10 @@ internal fun aRoomMemberIdentityStateChange(
4747

4848
internal fun anIdentityChangeState(
4949
roomMemberIdentityStateChanges: List<RoomMemberIdentityStateChange> = emptyList(),
50+
eventSink: (IdentityChangeEvent) -> Unit = {},
5051
) = IdentityChangeState(
5152
roomMemberIdentityStateChanges = roomMemberIdentityStateChanges.toImmutableList(),
52-
eventSink = {},
53+
eventSink = eventSink,
5354
)
5455

5556
internal fun anIdentityRoomMember(

0 commit comments

Comments
 (0)