Send failure verified user : display in ActionListView

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

View file

@ -241,6 +241,9 @@ fun MessagesView(
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
},
onEmojiReactionClick = ::onEmojiReactionClick,
onVerifiedUserSendFailureClick = { event ->
}
)
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.matrix.api.core.EventId
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -115,12 +116,14 @@ class DefaultActionListPresenter @AssistedInject constructor(
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
)
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
timelineItem.content.canReact()
if (actions.isNotEmpty() || displayEmojiReactions) {
val verifiedUserSendFailure = buildVerifiedUserSendFailure(timelineItem)
val displayEmojiReactions = usersEventPermissions.canSendReaction && timelineItem.content.canReact()
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != ActionListState.VerifiedUserSendFailure.None) {
target.value = ActionListState.Target.Success(
event = timelineItem,
displayEmojiReactions = displayEmojiReactions,
verifiedUserSendFailure = verifiedUserSendFailure,
actions = actions.toImmutableList()
)
} 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(
timelineItem: TimelineItem.Event,
usersEventPermissions: UserEventPermissions,

View file

@ -7,9 +7,12 @@
package io.element.android.features.messages.impl.actionlist
import androidx.compose.runtime.Composable
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.timeline.model.TimelineItem
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
@Immutable
@ -17,13 +20,31 @@ data class ActionListState(
val target: Target,
val eventSink: (ActionListEvents) -> Unit,
) {
@Immutable
sealed interface Target {
data object None : Target
data class Loading(val event: TimelineItem.Event) : Target
data class Success(
val event: TimelineItem.Event,
val displayEmojiReactions: Boolean,
val verifiedUserSendFailure: VerifiedUserSendFailure,
val actions: ImmutableList<TimelineItemAction>,
) : 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.aTimelineItemVideoContent
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -35,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState
),
displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
)
),
@ -47,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState,
),
displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
)
),
@ -56,6 +60,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState
),
displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
)
),
@ -65,6 +70,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState
),
displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
)
),
@ -74,6 +80,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState
),
displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
)
),
@ -83,6 +90,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState
),
displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
)
),
@ -92,6 +100,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState
),
displayEmojiReactions = true,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
)
),
@ -101,6 +110,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState
),
displayEmojiReactions = false,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
),
),
@ -110,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
reactionsState = reactionsState
),
displayEmojiReactions = false,
verifiedUserSendFailure = ActionListState.VerifiedUserSendFailure.None,
actions = aTimelineItemPollActionList(),
),
),
@ -120,6 +131,15 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
messageShield = MessageShield.UnknownDevice(isCritical = 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(),
)
),

View file

@ -27,6 +27,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@ -90,6 +91,7 @@ fun ActionListView(
onSelectAction: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
onEmojiReactionClick: (String, TimelineItem.Event) -> Unit,
onCustomReactionClick: (TimelineItem.Event) -> Unit,
onVerifiedUserSendFailureClick: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier,
) {
val sheetState = rememberModalBottomSheetState()
@ -126,6 +128,14 @@ fun ActionListView(
state.eventSink(ActionListEvents.Clear)
}
fun onVerifiedUserSendFailureClick() {
if (targetItem == null) return
sheetState.hide(coroutineScope) {
state.eventSink(ActionListEvents.Clear)
onVerifiedUserSendFailureClick(targetItem)
}
}
if (targetItem != null) {
ModalBottomSheet(
sheetState = sheetState,
@ -137,6 +147,7 @@ fun ActionListView(
onActionClick = ::onItemActionClick,
onEmojiReactionClick = ::onEmojiReactionClick,
onCustomReactionClick = ::onCustomReactionClick,
onVerifiedUserSendFailureClick = ::onVerifiedUserSendFailureClick,
modifier = Modifier
.navigationBarsPadding()
.imePadding()
@ -151,6 +162,7 @@ private fun SheetContent(
onActionClick: (TimelineItemAction) -> Unit,
onEmojiReactionClick: (String) -> Unit,
onCustomReactionClick: () -> Unit,
onVerifiedUserSendFailureClick: () -> Unit,
modifier: Modifier = Modifier,
) {
when (val target = state.target) {
@ -184,6 +196,16 @@ private fun SheetContent(
HorizontalDivider()
}
}
if (target.verifiedUserSendFailure != ActionListState.VerifiedUserSendFailure.None) {
item {
VerifiedUserSendFailureView(
sendFailure = target.verifiedUserSendFailure,
modifier = Modifier.fillMaxWidth(),
onClick = onVerifiedUserSendFailureClick
)
HorizontalDivider()
}
}
if (target.displayEmojiReactions) {
item {
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
private fun EmojiButton(
emoji: String,
@ -387,5 +436,6 @@ internal fun SheetContentPreview(
onActionClick = {},
onEmojiReactionClick = {},
onCustomReactionClick = {},
onVerifiedUserSendFailureClick = {},
)
}

View file

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

View file

@ -36,6 +36,7 @@
<string name="action_back">"Back"</string>
<string name="action_call">"Call"</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_clear">"Clear"</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_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_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_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>
@ -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_osm_maps">"Open in OpenStreetMap"</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="settings_version_number">"Version: %1$s (%2$s)"</string>
<string name="test_language_identifier">"en"</string>