Skip to content

Commit e0bc026

Browse files
committed
Send failure verified user : display in ActionListView
1 parent b2c7ea0 commit e0bc026

File tree

7 files changed

+136
-3
lines changed

7 files changed

+136
-3
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+
246+
}
244247
)
245248

246249
CustomReactionBottomSheet(

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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import io.element.android.libraries.architecture.Presenter
3636
import io.element.android.libraries.di.RoomScope
3737
import io.element.android.libraries.matrix.api.core.EventId
3838
import io.element.android.libraries.matrix.api.room.MatrixRoom
39+
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
3940
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
4041
import kotlinx.collections.immutable.ImmutableList
4142
import kotlinx.collections.immutable.persistentListOf
@@ -115,19 +116,47 @@ class DefaultActionListPresenter @AssistedInject constructor(
115116
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
116117
)
117118

118-
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
119-
timelineItem.content.canReact()
120-
if (actions.isNotEmpty() || displayEmojiReactions) {
119+
val verifiedUserSendFailure = buildVerifiedUserSendFailure(timelineItem)
120+
val displayEmojiReactions = usersEventPermissions.canSendReaction && timelineItem.content.canReact()
121+
122+
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != ActionListState.VerifiedUserSendFailure.None) {
121123
target.value = ActionListState.Target.Success(
122124
event = timelineItem,
123125
displayEmojiReactions = displayEmojiReactions,
126+
verifiedUserSendFailure = verifiedUserSendFailure,
124127
actions = actions.toImmutableList()
125128
)
126129
} else {
127130
target.value = ActionListState.Target.None
128131
}
129132
}
130133

134+
private suspend fun buildVerifiedUserSendFailure(
135+
timelineItem: TimelineItem.Event,
136+
): ActionListState.VerifiedUserSendFailure {
137+
return when (val sendState = timelineItem.localSendState) {
138+
is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> {
139+
val userId = sendState.devices.keys.firstOrNull()
140+
if (userId == null) {
141+
ActionListState.VerifiedUserSendFailure.None
142+
} else {
143+
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
144+
ActionListState.VerifiedUserSendFailure.UnsignedDevice(displayName)
145+
}
146+
}
147+
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> {
148+
val userId = sendState.users.firstOrNull()
149+
if (userId == null) {
150+
ActionListState.VerifiedUserSendFailure.None
151+
} else {
152+
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
153+
ActionListState.VerifiedUserSendFailure.ChangedIdentity(displayName)
154+
}
155+
}
156+
else -> ActionListState.VerifiedUserSendFailure.None
157+
}
158+
}
159+
131160
private fun buildActions(
132161
timelineItem: TimelineItem.Event,
133162
usersEventPermissions: UserEventPermissions,

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,44 @@
77

88
package io.element.android.features.messages.impl.actionlist
99

10+
import androidx.compose.runtime.Composable
1011
import androidx.compose.runtime.Immutable
12+
import androidx.compose.ui.res.stringResource
1113
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
1214
import io.element.android.features.messages.impl.timeline.model.TimelineItem
15+
import io.element.android.libraries.ui.strings.CommonStrings
1316
import kotlinx.collections.immutable.ImmutableList
1417

1518
@Immutable
1619
data class ActionListState(
1720
val target: Target,
1821
val eventSink: (ActionListEvents) -> Unit,
1922
) {
23+
@Immutable
2024
sealed interface Target {
2125
data object None : Target
2226
data class Loading(val event: TimelineItem.Event) : Target
2327
data class Success(
2428
val event: TimelineItem.Event,
2529
val displayEmojiReactions: Boolean,
30+
val verifiedUserSendFailure: VerifiedUserSendFailure,
2631
val actions: ImmutableList<TimelineItemAction>,
2732
) : Target
2833
}
34+
35+
@Immutable
36+
sealed interface VerifiedUserSendFailure {
37+
data object None : VerifiedUserSendFailure
38+
data class UnsignedDevice(val displayName: String) : VerifiedUserSendFailure
39+
data class ChangedIdentity(val displayName: String) : VerifiedUserSendFailure
40+
41+
@Composable
42+
fun formatted(): String {
43+
return when (this) {
44+
is None -> ""
45+
is UnsignedDevice -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, displayName)
46+
is ChangedIdentity -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, displayName)
47+
}
48+
}
49+
}
2950
}

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
@@ -18,6 +18,8 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
1818
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
1919
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
2020
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
21+
import io.element.android.libraries.matrix.api.core.UserId
22+
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
2123
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
2224
import kotlinx.collections.immutable.ImmutableList
2325
import kotlinx.collections.immutable.persistentListOf
@@ -35,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
3537
reactionsState = reactionsState
3638
),
3739
displayEmojiReactions = true,
40+
verifiedUserSendFailure = ActionListState.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 = ActionListState.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 = ActionListState.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 = ActionListState.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 = ActionListState.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 = ActionListState.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 = ActionListState.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 = ActionListState.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 = ActionListState.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 = ActionListState.VerifiedUserSendFailure.None,
135+
actions = aTimelineItemActionList(),
136+
)
137+
),
138+
anActionListState().copy(
139+
target = ActionListState.Target.Success(
140+
event = aTimelineItemEvent(),
141+
displayEmojiReactions = true,
142+
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.UnsignedDevice(displayName = "Alice"),
123143
actions = aTimelineItemActionList(),
124144
)
125145
),

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

Lines changed: 50 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
@@ -90,6 +91,7 @@ fun ActionListView(
9091
onSelectAction: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
9192
onEmojiReactionClick: (String, TimelineItem.Event) -> Unit,
9293
onCustomReactionClick: (TimelineItem.Event) -> Unit,
94+
onVerifiedUserSendFailureClick: (TimelineItem.Event) -> Unit,
9395
modifier: Modifier = Modifier,
9496
) {
9597
val sheetState = rememberModalBottomSheetState()
@@ -126,6 +128,14 @@ fun ActionListView(
126128
state.eventSink(ActionListEvents.Clear)
127129
}
128130

131+
fun onVerifiedUserSendFailureClick() {
132+
if (targetItem == null) return
133+
sheetState.hide(coroutineScope) {
134+
state.eventSink(ActionListEvents.Clear)
135+
onVerifiedUserSendFailureClick(targetItem)
136+
}
137+
}
138+
129139
if (targetItem != null) {
130140
ModalBottomSheet(
131141
sheetState = sheetState,
@@ -137,6 +147,7 @@ fun ActionListView(
137147
onActionClick = ::onItemActionClick,
138148
onEmojiReactionClick = ::onEmojiReactionClick,
139149
onCustomReactionClick = ::onCustomReactionClick,
150+
onVerifiedUserSendFailureClick = ::onVerifiedUserSendFailureClick,
140151
modifier = Modifier
141152
.navigationBarsPadding()
142153
.imePadding()
@@ -151,6 +162,7 @@ private fun SheetContent(
151162
onActionClick: (TimelineItemAction) -> Unit,
152163
onEmojiReactionClick: (String) -> Unit,
153164
onCustomReactionClick: () -> Unit,
165+
onVerifiedUserSendFailureClick: () -> Unit,
154166
modifier: Modifier = Modifier,
155167
) {
156168
when (val target = state.target) {
@@ -184,6 +196,16 @@ private fun SheetContent(
184196
HorizontalDivider()
185197
}
186198
}
199+
if (target.verifiedUserSendFailure != ActionListState.VerifiedUserSendFailure.None) {
200+
item {
201+
VerifiedUserSendFailureView(
202+
sendFailure = target.verifiedUserSendFailure,
203+
modifier = Modifier.fillMaxWidth(),
204+
onClick = onVerifiedUserSendFailureClick
205+
)
206+
HorizontalDivider()
207+
}
208+
}
187209
if (target.displayEmojiReactions) {
188210
item {
189211
EmojiReactionsRow(
@@ -338,6 +360,33 @@ private fun EmojiReactionsRow(
338360
}
339361
}
340362

363+
@Composable
364+
private fun VerifiedUserSendFailureView(
365+
sendFailure: ActionListState.VerifiedUserSendFailure,
366+
onClick: () -> Unit,
367+
modifier: Modifier = Modifier,
368+
) {
369+
ListItem(
370+
modifier = modifier
371+
.clickable(onClick = onClick)
372+
.padding(horizontal = 16.dp, vertical = 8.dp),
373+
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Error())),
374+
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChevronRight())),
375+
headlineContent = {
376+
Text(
377+
text = sendFailure.formatted(),
378+
style = ElementTheme.typography.fontBodySmMedium,
379+
)
380+
},
381+
colors = ListItemDefaults.colors(
382+
containerColor = Color.Transparent,
383+
leadingIconColor = ElementTheme.colors.iconCriticalPrimary,
384+
trailingIconColor = ElementTheme.colors.iconPrimary,
385+
headlineColor = ElementTheme.colors.textCriticalPrimary,
386+
),
387+
)
388+
}
389+
341390
@Composable
342391
private fun EmojiButton(
343392
emoji: String,
@@ -387,5 +436,6 @@ internal fun SheetContentPreview(
387436
onActionClick = {},
388437
onEmojiReactionClick = {},
389438
onCustomReactionClick = {},
439+
onVerifiedUserSendFailureClick = {},
390440
)
391441
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ private fun PinnedMessagesListLoaded(
181181
onSelectAction = ::onActionSelected,
182182
onCustomReactionClick = {},
183183
onEmojiReactionClick = { _, _ -> },
184+
onVerifiedUserSendFailureClick = {}
184185
)
185186
LazyColumn(
186187
modifier = modifier.fillMaxSize(),

libraries/ui-strings/src/main/res/values/localazy.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<string name="action_back">"Back"</string>
3737
<string name="action_call">"Call"</string>
3838
<string name="action_cancel">"Cancel"</string>
39+
<string name="action_cancel_for_now">"Cancel for now"</string>
3940
<string name="action_choose_photo">"Choose photo"</string>
4041
<string name="action_clear">"Clear"</string>
4142
<string name="action_close">"Close"</string>
@@ -283,6 +284,12 @@ Reason: %1$s."</string>
283284
<string name="screen_pinned_timeline_screen_title_empty">"Pinned messages"</string>
284285
<string name="screen_reset_identity_confirmation_subtitle">"You\'re about to go to your %1$s account to reset your identity. Afterwards you\'ll be taken back to the app."</string>
285286
<string name="screen_reset_identity_confirmation_title">"Can\'t confirm? Go to your account to reset your identity."</string>
287+
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Withdraw verification and send"</string>
288+
<string name="screen_resolve_send_failure_changed_identity_subtitle">"You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$s."</string>
289+
<string name="screen_resolve_send_failure_changed_identity_title">"Your message was not sent because %1$s’s verified identity has changed"</string>
290+
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"Send message anyway"</string>
291+
<string name="screen_resolve_send_failure_unsigned_device_subtitle">"%1$s is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$s has verified all their devices."</string>
292+
<string name="screen_resolve_send_failure_unsigned_device_title">"Your message was not sent because %1$s has not verified one or more devices"</string>
286293
<string name="screen_room_details_pinned_events_row_title">"Pinned messages"</string>
287294
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
288295
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
@@ -304,6 +311,8 @@ Reason: %1$s."</string>
304311
<string name="screen_share_open_google_maps">"Open in Google Maps"</string>
305312
<string name="screen_share_open_osm_maps">"Open in OpenStreetMap"</string>
306313
<string name="screen_share_this_location_action">"Share this location"</string>
314+
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Message not sent because %1$s’s verified identity has changed."</string>
315+
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Message not sent because %1$s has not verified one or more devices."</string>
307316
<string name="screen_view_location_title">"Location"</string>
308317
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
309318
<string name="test_language_identifier">"en"</string>

0 commit comments

Comments
 (0)