Send failure verified user : display in ActionListView

This commit is contained in:
ganfra 2024-09-12 17:24:01 +02:00
parent 37d4e586d6
commit d215978241
7 changed files with 136 additions and 3 deletions

View file

@ -241,6 +241,9 @@ fun MessagesView(
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event)) state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
}, },
onEmojiReactionClick = ::onEmojiReactionClick, onEmojiReactionClick = ::onEmojiReactionClick,
onVerifiedUserSendFailureClick = { event ->
}
) )
CustomReactionBottomSheet( CustomReactionBottomSheet(

View file

@ -36,6 +36,7 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -115,12 +116,14 @@ class DefaultActionListPresenter @AssistedInject constructor(
isEventPinned = pinnedEventIds.contains(timelineItem.eventId), isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
) )
val displayEmojiReactions = usersEventPermissions.canSendReaction && val verifiedUserSendFailure = buildVerifiedUserSendFailure(timelineItem)
timelineItem.content.canReact() val displayEmojiReactions = usersEventPermissions.canSendReaction && timelineItem.content.canReact()
if (actions.isNotEmpty() || displayEmojiReactions) {
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != ActionListState.VerifiedUserSendFailure.None) {
target.value = ActionListState.Target.Success( target.value = ActionListState.Target.Success(
event = timelineItem, event = timelineItem,
displayEmojiReactions = displayEmojiReactions, displayEmojiReactions = displayEmojiReactions,
verifiedUserSendFailure = verifiedUserSendFailure,
actions = actions.toImmutableList() actions = actions.toImmutableList()
) )
} else { } else {
@ -128,6 +131,32 @@ class DefaultActionListPresenter @AssistedInject constructor(
} }
} }
private suspend fun buildVerifiedUserSendFailure(
timelineItem: TimelineItem.Event,
): ActionListState.VerifiedUserSendFailure {
return when (val sendState = timelineItem.localSendState) {
is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> {
val userId = sendState.devices.keys.firstOrNull()
if (userId == null) {
ActionListState.VerifiedUserSendFailure.None
} else {
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
ActionListState.VerifiedUserSendFailure.UnsignedDevice(displayName)
}
}
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> {
val userId = sendState.users.firstOrNull()
if (userId == null) {
ActionListState.VerifiedUserSendFailure.None
} else {
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
ActionListState.VerifiedUserSendFailure.ChangedIdentity(displayName)
}
}
else -> ActionListState.VerifiedUserSendFailure.None
}
}
private fun buildActions( private fun buildActions(
timelineItem: TimelineItem.Event, timelineItem: TimelineItem.Event,
usersEventPermissions: UserEventPermissions, usersEventPermissions: UserEventPermissions,

View file

@ -7,9 +7,12 @@
package io.element.android.features.messages.impl.actionlist package io.element.android.features.messages.impl.actionlist
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable
@ -17,13 +20,31 @@ data class ActionListState(
val target: Target, val target: Target,
val eventSink: (ActionListEvents) -> Unit, val eventSink: (ActionListEvents) -> Unit,
) { ) {
@Immutable
sealed interface Target { sealed interface Target {
data object None : Target data object None : Target
data class Loading(val event: TimelineItem.Event) : Target data class Loading(val event: TimelineItem.Event) : Target
data class Success( data class Success(
val event: TimelineItem.Event, val event: TimelineItem.Event,
val displayEmojiReactions: Boolean, val displayEmojiReactions: Boolean,
val verifiedUserSendFailure: VerifiedUserSendFailure,
val actions: ImmutableList<TimelineItemAction>, val actions: ImmutableList<TimelineItemAction>,
) : Target ) : Target
} }
@Immutable
sealed interface VerifiedUserSendFailure {
data object None : VerifiedUserSendFailure
data class UnsignedDevice(val displayName: String) : VerifiedUserSendFailure
data class ChangedIdentity(val displayName: String) : VerifiedUserSendFailure
@Composable
fun formatted(): String {
return when (this) {
is None -> ""
is UnsignedDevice -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, displayName)
is ChangedIdentity -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, displayName)
}
}
}
} }

View file

@ -18,6 +18,8 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -35,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState reactionsState = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
@ -47,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState, reactionsState = reactionsState,
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
@ -56,6 +60,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState reactionsState = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
@ -65,6 +70,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState reactionsState = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
@ -74,6 +80,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState reactionsState = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
@ -83,6 +90,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState reactionsState = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
@ -92,6 +100,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState reactionsState = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
@ -101,6 +110,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState reactionsState = reactionsState
), ),
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
), ),
), ),
@ -110,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState reactionsState = reactionsState
), ),
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemPollActionList(), actions = aTimelineItemPollActionList(),
), ),
), ),
@ -120,6 +131,15 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
messageShield = MessageShield.UnknownDevice(isCritical = true) messageShield = MessageShield.UnknownDevice(isCritical = true)
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
)
),
anActionListState().copy(
target = ActionListState.Target.Success(
event = aTimelineItemEvent(),
displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.UnsignedDevice(displayName = "Alice"),
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),

View file

@ -27,6 +27,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -90,6 +91,7 @@ fun ActionListView(
onSelectAction: (action: TimelineItemAction, TimelineItem.Event) -> Unit, onSelectAction: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
onEmojiReactionClick: (String, TimelineItem.Event) -> Unit, onEmojiReactionClick: (String, TimelineItem.Event) -> Unit,
onCustomReactionClick: (TimelineItem.Event) -> Unit, onCustomReactionClick: (TimelineItem.Event) -> Unit,
onVerifiedUserSendFailureClick: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
@ -126,6 +128,14 @@ fun ActionListView(
state.eventSink(ActionListEvents.Clear) state.eventSink(ActionListEvents.Clear)
} }
fun onVerifiedUserSendFailureClick() {
if (targetItem == null) return
sheetState.hide(coroutineScope) {
state.eventSink(ActionListEvents.Clear)
onVerifiedUserSendFailureClick(targetItem)
}
}
if (targetItem != null) { if (targetItem != null) {
ModalBottomSheet( ModalBottomSheet(
sheetState = sheetState, sheetState = sheetState,
@ -137,6 +147,7 @@ fun ActionListView(
onActionClick = ::onItemActionClick, onActionClick = ::onItemActionClick,
onEmojiReactionClick = ::onEmojiReactionClick, onEmojiReactionClick = ::onEmojiReactionClick,
onCustomReactionClick = ::onCustomReactionClick, onCustomReactionClick = ::onCustomReactionClick,
onVerifiedUserSendFailureClick = ::onVerifiedUserSendFailureClick,
modifier = Modifier modifier = Modifier
.navigationBarsPadding() .navigationBarsPadding()
.imePadding() .imePadding()
@ -151,6 +162,7 @@ private fun SheetContent(
onActionClick: (TimelineItemAction) -> Unit, onActionClick: (TimelineItemAction) -> Unit,
onEmojiReactionClick: (String) -> Unit, onEmojiReactionClick: (String) -> Unit,
onCustomReactionClick: () -> Unit, onCustomReactionClick: () -> Unit,
onVerifiedUserSendFailureClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (val target = state.target) { when (val target = state.target) {
@ -184,6 +196,16 @@ private fun SheetContent(
HorizontalDivider() HorizontalDivider()
} }
} }
if (target.verifiedUserSendFailure != ActionListState.VerifiedUserSendFailure.None) {
item {
VerifiedUserSendFailureView(
sendFailure = target.verifiedUserSendFailure,
modifier = Modifier.fillMaxWidth(),
onClick = onVerifiedUserSendFailureClick
)
HorizontalDivider()
}
}
if (target.displayEmojiReactions) { if (target.displayEmojiReactions) {
item { item {
EmojiReactionsRow( EmojiReactionsRow(
@ -338,6 +360,33 @@ private fun EmojiReactionsRow(
} }
} }
@Composable
private fun VerifiedUserSendFailureView(
sendFailure: ActionListState.VerifiedUserSendFailure,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
ListItem(
modifier = modifier
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Error())),
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChevronRight())),
headlineContent = {
Text(
text = sendFailure.formatted(),
style = ElementTheme.typography.fontBodySmMedium,
)
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
leadingIconColor = ElementTheme.colors.iconCriticalPrimary,
trailingIconColor = ElementTheme.colors.iconPrimary,
headlineColor = ElementTheme.colors.textCriticalPrimary,
),
)
}
@Composable @Composable
private fun EmojiButton( private fun EmojiButton(
emoji: String, emoji: String,
@ -387,5 +436,6 @@ internal fun SheetContentPreview(
onActionClick = {}, onActionClick = {},
onEmojiReactionClick = {}, onEmojiReactionClick = {},
onCustomReactionClick = {}, onCustomReactionClick = {},
onVerifiedUserSendFailureClick = {},
) )
} }

View file

@ -181,6 +181,7 @@ private fun PinnedMessagesListLoaded(
onSelectAction = ::onActionSelected, onSelectAction = ::onActionSelected,
onCustomReactionClick = {}, onCustomReactionClick = {},
onEmojiReactionClick = { _, _ -> }, onEmojiReactionClick = { _, _ -> },
onVerifiedUserSendFailureClick = {}
) )
LazyColumn( LazyColumn(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),

View file

@ -36,6 +36,7 @@
<string name="action_back">"Back"</string> <string name="action_back">"Back"</string>
<string name="action_call">"Call"</string> <string name="action_call">"Call"</string>
<string name="action_cancel">"Cancel"</string> <string name="action_cancel">"Cancel"</string>
<string name="action_cancel_for_now">"Cancel for now"</string>
<string name="action_choose_photo">"Choose photo"</string> <string name="action_choose_photo">"Choose photo"</string>
<string name="action_clear">"Clear"</string> <string name="action_clear">"Clear"</string>
<string name="action_close">"Close"</string> <string name="action_close">"Close"</string>
@ -283,6 +284,12 @@ Reason: %1$s."</string>
<string name="screen_pinned_timeline_screen_title_empty">"Pinned messages"</string> <string name="screen_pinned_timeline_screen_title_empty">"Pinned messages"</string>
<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> <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>
<string name="screen_reset_identity_confirmation_title">"Can\'t confirm? Go to your account to reset your identity."</string> <string name="screen_reset_identity_confirmation_title">"Can\'t confirm? Go to your account to reset your identity."</string>
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Withdraw verification and send"</string>
<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>
<string name="screen_resolve_send_failure_changed_identity_title">"Your message was not sent because %1$ss verified identity has changed"</string>
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"Send message anyway"</string>
<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>
<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>
<string name="screen_room_details_pinned_events_row_title">"Pinned messages"</string> <string name="screen_room_details_pinned_events_row_title">"Pinned messages"</string>
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string> <string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string> <string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
@ -304,6 +311,8 @@ Reason: %1$s."</string>
<string name="screen_share_open_google_maps">"Open in Google Maps"</string> <string name="screen_share_open_google_maps">"Open in Google Maps"</string>
<string name="screen_share_open_osm_maps">"Open in OpenStreetMap"</string> <string name="screen_share_open_osm_maps">"Open in OpenStreetMap"</string>
<string name="screen_share_this_location_action">"Share this location"</string> <string name="screen_share_this_location_action">"Share this location"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Message not sent because %1$ss verified identity has changed."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Message not sent because %1$s has not verified one or more devices."</string>
<string name="screen_view_location_title">"Location"</string> <string name="screen_view_location_title">"Location"</string>
<string name="settings_version_number">"Version: %1$s (%2$s)"</string> <string name="settings_version_number">"Version: %1$s (%2$s)"</string>
<string name="test_language_identifier">"en"</string> <string name="test_language_identifier">"en"</string>