Skip to content

Commit 47d0c50

Browse files
authored
Merge pull request #3461 from element-hq/feature/fga/send_failure_identity_changes
Require acknowledgement to send to a verified user if their identity changed or if a device is unverified.
2 parents 7238af7 + be3ead0 commit 47d0c50

File tree

50 files changed

+1354
-86
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1354
-86
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ fun MessagesView(
241241
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
242242
},
243243
onEmojiReactionClick = ::onEmojiReactionClick,
244+
onVerifiedUserSendFailureClick = { event ->
245+
state.timelineState.eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
246+
},
244247
)
245248

246249
CustomReactionBottomSheet(

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEn
2222
import io.element.android.features.messages.impl.UserEventPermissions
2323
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
2424
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
25+
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
26+
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
2527
import io.element.android.features.messages.impl.timeline.model.TimelineItem
2628
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
2729
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
@@ -56,6 +58,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
5658
private val appPreferencesStore: AppPreferencesStore,
5759
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
5860
private val room: MatrixRoom,
61+
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
5962
) : ActionListPresenter {
6063
@AssistedFactory
6164
@ContributesBinding(RoomScope::class)
@@ -115,12 +118,14 @@ class DefaultActionListPresenter @AssistedInject constructor(
115118
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
116119
)
117120

118-
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
119-
timelineItem.content.canReact()
120-
if (actions.isNotEmpty() || displayEmojiReactions) {
121+
val verifiedUserSendFailure = userSendFailureFactory.create(timelineItem.localSendState)
122+
val displayEmojiReactions = usersEventPermissions.canSendReaction && timelineItem.content.canReact()
123+
124+
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) {
121125
target.value = ActionListState.Target.Success(
122126
event = timelineItem,
123127
displayEmojiReactions = displayEmojiReactions,
128+
verifiedUserSendFailure = verifiedUserSendFailure,
124129
actions = actions.toImmutableList()
125130
)
126131
} else {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.actionlist
99

1010
import androidx.compose.runtime.Immutable
1111
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
12+
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
1213
import io.element.android.features.messages.impl.timeline.model.TimelineItem
1314
import kotlinx.collections.immutable.ImmutableList
1415

@@ -17,12 +18,14 @@ data class ActionListState(
1718
val target: Target,
1819
val eventSink: (ActionListEvents) -> Unit,
1920
) {
21+
@Immutable
2022
sealed interface Target {
2123
data object None : Target
2224
data class Loading(val event: TimelineItem.Event) : Target
2325
data class Success(
2426
val event: TimelineItem.Event,
2527
val displayEmojiReactions: Boolean,
28+
val verifiedUserSendFailure: VerifiedUserSendFailure,
2629
val actions: ImmutableList<TimelineItemAction>,
2730
) : Target
2831
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ package io.element.android.features.messages.impl.actionlist
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
1111
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
12+
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
13+
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.anUnsignedDeviceSendFailure
1214
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
1315
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
1416
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent
@@ -35,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
3537
reactionsState = reactionsState
3638
),
3739
displayEmojiReactions = true,
40+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
3841
actions = aTimelineItemActionList(),
3942
)
4043
),
@@ -47,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
4750
reactionsState = reactionsState,
4851
),
4952
displayEmojiReactions = true,
53+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
5054
actions = aTimelineItemActionList(),
5155
)
5256
),
@@ -56,6 +60,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
5660
reactionsState = reactionsState
5761
),
5862
displayEmojiReactions = true,
63+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
5964
actions = aTimelineItemActionList(),
6065
)
6166
),
@@ -65,6 +70,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
6570
reactionsState = reactionsState
6671
),
6772
displayEmojiReactions = true,
73+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
6874
actions = aTimelineItemActionList(),
6975
)
7076
),
@@ -74,6 +80,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
7480
reactionsState = reactionsState
7581
),
7682
displayEmojiReactions = true,
83+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
7784
actions = aTimelineItemActionList(),
7885
)
7986
),
@@ -83,6 +90,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
8390
reactionsState = reactionsState
8491
),
8592
displayEmojiReactions = true,
93+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
8694
actions = aTimelineItemActionList(),
8795
)
8896
),
@@ -92,6 +100,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
92100
reactionsState = reactionsState
93101
),
94102
displayEmojiReactions = true,
103+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
95104
actions = aTimelineItemActionList(),
96105
)
97106
),
@@ -101,6 +110,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
101110
reactionsState = reactionsState
102111
),
103112
displayEmojiReactions = false,
113+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
104114
actions = aTimelineItemActionList(),
105115
),
106116
),
@@ -110,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
110120
reactionsState = reactionsState
111121
),
112122
displayEmojiReactions = false,
123+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
113124
actions = aTimelineItemPollActionList(),
114125
),
115126
),
@@ -120,6 +131,15 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
120131
messageShield = MessageShield.UnknownDevice(isCritical = true)
121132
),
122133
displayEmojiReactions = true,
134+
verifiedUserSendFailure = VerifiedUserSendFailure.None,
135+
actions = aTimelineItemActionList(),
136+
)
137+
),
138+
anActionListState().copy(
139+
target = ActionListState.Target.Success(
140+
event = aTimelineItemEvent(),
141+
displayEmojiReactions = true,
142+
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
123143
actions = aTimelineItemActionList(),
124144
)
125145
),

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.compose.foundation.lazy.items
2727
import androidx.compose.foundation.shape.CircleShape
2828
import androidx.compose.material.ripple.rememberRipple
2929
import androidx.compose.material3.ExperimentalMaterial3Api
30+
import androidx.compose.material3.ListItemDefaults
3031
import androidx.compose.material3.MaterialTheme
3132
import androidx.compose.material3.rememberModalBottomSheetState
3233
import androidx.compose.runtime.Composable
@@ -46,6 +47,10 @@ import androidx.compose.ui.unit.dp
4647
import io.element.android.compound.theme.ElementTheme
4748
import io.element.android.compound.tokens.generated.CompoundIcons
4849
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
50+
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
51+
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.ChangedIdentity
52+
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.None
53+
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.UnsignedDevice
4954
import io.element.android.features.messages.impl.timeline.components.MessageShieldView
5055
import io.element.android.features.messages.impl.timeline.model.TimelineItem
5156
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
@@ -90,6 +95,7 @@ fun ActionListView(
9095
onSelectAction: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
9196
onEmojiReactionClick: (String, TimelineItem.Event) -> Unit,
9297
onCustomReactionClick: (TimelineItem.Event) -> Unit,
98+
onVerifiedUserSendFailureClick: (TimelineItem.Event) -> Unit,
9399
modifier: Modifier = Modifier,
94100
) {
95101
val sheetState = rememberModalBottomSheetState()
@@ -126,6 +132,14 @@ fun ActionListView(
126132
state.eventSink(ActionListEvents.Clear)
127133
}
128134

135+
fun onVerifiedUserSendFailureClick() {
136+
if (targetItem == null) return
137+
sheetState.hide(coroutineScope) {
138+
state.eventSink(ActionListEvents.Clear)
139+
onVerifiedUserSendFailureClick(targetItem)
140+
}
141+
}
142+
129143
if (targetItem != null) {
130144
ModalBottomSheet(
131145
sheetState = sheetState,
@@ -137,6 +151,7 @@ fun ActionListView(
137151
onActionClick = ::onItemActionClick,
138152
onEmojiReactionClick = ::onEmojiReactionClick,
139153
onCustomReactionClick = ::onCustomReactionClick,
154+
onVerifiedUserSendFailureClick = ::onVerifiedUserSendFailureClick,
140155
modifier = Modifier
141156
.navigationBarsPadding()
142157
.imePadding()
@@ -151,6 +166,7 @@ private fun SheetContent(
151166
onActionClick: (TimelineItemAction) -> Unit,
152167
onEmojiReactionClick: (String) -> Unit,
153168
onCustomReactionClick: () -> Unit,
169+
onVerifiedUserSendFailureClick: () -> Unit,
154170
modifier: Modifier = Modifier,
155171
) {
156172
when (val target = state.target) {
@@ -184,6 +200,16 @@ private fun SheetContent(
184200
HorizontalDivider()
185201
}
186202
}
203+
if (target.verifiedUserSendFailure != None) {
204+
item {
205+
VerifiedUserSendFailureView(
206+
sendFailure = target.verifiedUserSendFailure,
207+
modifier = Modifier.fillMaxWidth(),
208+
onClick = onVerifiedUserSendFailureClick
209+
)
210+
HorizontalDivider()
211+
}
212+
}
187213
if (target.displayEmojiReactions) {
188214
item {
189215
EmojiReactionsRow(
@@ -338,6 +364,42 @@ private fun EmojiReactionsRow(
338364
}
339365
}
340366

367+
@Composable
368+
private fun VerifiedUserSendFailureView(
369+
sendFailure: VerifiedUserSendFailure,
370+
onClick: () -> Unit,
371+
modifier: Modifier = Modifier,
372+
) {
373+
@Composable
374+
fun VerifiedUserSendFailure.headline(): String {
375+
return when (this) {
376+
is None -> ""
377+
is UnsignedDevice -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, userDisplayName)
378+
is ChangedIdentity -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, userDisplayName)
379+
}
380+
}
381+
382+
ListItem(
383+
modifier = modifier
384+
.clickable(onClick = onClick)
385+
.padding(horizontal = 16.dp, vertical = 8.dp),
386+
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Error())),
387+
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChevronRight())),
388+
headlineContent = {
389+
Text(
390+
text = sendFailure.headline(),
391+
style = ElementTheme.typography.fontBodySmMedium,
392+
)
393+
},
394+
colors = ListItemDefaults.colors(
395+
containerColor = Color.Transparent,
396+
leadingIconColor = ElementTheme.colors.iconCriticalPrimary,
397+
trailingIconColor = ElementTheme.colors.iconPrimary,
398+
headlineColor = ElementTheme.colors.textCriticalPrimary,
399+
),
400+
)
401+
}
402+
341403
@Composable
342404
private fun EmojiButton(
343405
emoji: String,
@@ -387,5 +449,6 @@ internal fun SheetContentPreview(
387449
onActionClick = {},
388450
onEmojiReactionClick = {},
389451
onCustomReactionClick = {},
452+
onVerifiedUserSendFailureClick = {},
390453
)
391454
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.impl.crypto.sendfailure
9+
10+
import androidx.compose.runtime.Immutable
11+
12+
@Immutable
13+
sealed interface VerifiedUserSendFailure {
14+
data object None : VerifiedUserSendFailure
15+
16+
data class UnsignedDevice(
17+
val userDisplayName: String,
18+
) : VerifiedUserSendFailure
19+
20+
data class ChangedIdentity(
21+
val userDisplayName: String,
22+
) : VerifiedUserSendFailure
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.impl.crypto.sendfailure
9+
10+
import io.element.android.libraries.matrix.api.room.MatrixRoom
11+
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
12+
import javax.inject.Inject
13+
14+
class VerifiedUserSendFailureFactory @Inject constructor(
15+
private val room: MatrixRoom,
16+
) {
17+
suspend fun create(
18+
sendState: LocalEventSendState?,
19+
): VerifiedUserSendFailure {
20+
return when (sendState) {
21+
is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> {
22+
val userId = sendState.devices.keys.firstOrNull()
23+
if (userId == null) {
24+
VerifiedUserSendFailure.None
25+
} else {
26+
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
27+
VerifiedUserSendFailure.UnsignedDevice(displayName)
28+
}
29+
}
30+
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> {
31+
val userId = sendState.users.firstOrNull()
32+
if (userId == null) {
33+
VerifiedUserSendFailure.None
34+
} else {
35+
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
36+
VerifiedUserSendFailure.ChangedIdentity(displayName)
37+
}
38+
}
39+
else -> VerifiedUserSendFailure.None
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
9+
10+
import io.element.android.features.messages.impl.timeline.model.TimelineItem
11+
12+
sealed interface ResolveVerifiedUserSendFailureEvents {
13+
data class ComputeForMessage(
14+
val messageEvent: TimelineItem.Event,
15+
) : ResolveVerifiedUserSendFailureEvents
16+
17+
data object ResolveAndResend : ResolveVerifiedUserSendFailureEvents
18+
data object Retry : ResolveVerifiedUserSendFailureEvents
19+
data object Dismiss : ResolveVerifiedUserSendFailureEvents
20+
}

0 commit comments

Comments
 (0)