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.
This commit is contained in:
commit
60365e46a5
50 changed files with 1355 additions and 87 deletions
|
|
@ -241,6 +241,9 @@ fun MessagesView(
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||||
},
|
},
|
||||||
onEmojiReactionClick = ::onEmojiReactionClick,
|
onEmojiReactionClick = ::onEmojiReactionClick,
|
||||||
|
onVerifiedUserSendFailureClick = { event ->
|
||||||
|
state.timelineState.eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
CustomReactionBottomSheet(
|
CustomReactionBottomSheet(
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEn
|
||||||
import io.element.android.features.messages.impl.UserEventPermissions
|
import io.element.android.features.messages.impl.UserEventPermissions
|
||||||
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.actionlist.model.TimelineItemActionPostProcessor
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
|
|
@ -56,6 +58,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
|
||||||
) : ActionListPresenter {
|
) : ActionListPresenter {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ContributesBinding(RoomScope::class)
|
@ContributesBinding(RoomScope::class)
|
||||||
|
|
@ -115,12 +118,14 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
|
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
|
||||||
)
|
)
|
||||||
|
|
||||||
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
|
val verifiedUserSendFailure = userSendFailureFactory.create(timelineItem.localSendState)
|
||||||
timelineItem.content.canReact()
|
val displayEmojiReactions = usersEventPermissions.canSendReaction && timelineItem.content.canReact()
|
||||||
if (actions.isNotEmpty() || displayEmojiReactions) {
|
|
||||||
|
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != 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 {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.actionlist
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
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.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -17,12 +18,14 @@ 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ package io.element.android.features.messages.impl.actionlist
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
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.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.anUnsignedDeviceSendFailure
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent
|
||||||
|
|
@ -35,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState
|
reactionsState = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -47,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState,
|
reactionsState = reactionsState,
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -56,6 +60,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState
|
reactionsState = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -65,6 +70,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState
|
reactionsState = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -74,6 +80,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState
|
reactionsState = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -83,6 +90,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState
|
reactionsState = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -92,6 +100,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState
|
reactionsState = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -101,6 +110,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState
|
reactionsState = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -110,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
reactionsState = reactionsState
|
reactionsState = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
|
verifiedUserSendFailure = 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 = VerifiedUserSendFailure.None,
|
||||||
|
actions = aTimelineItemActionList(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
anActionListState().copy(
|
||||||
|
target = ActionListState.Target.Success(
|
||||||
|
event = aTimelineItemEvent(),
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -46,6 +47,10 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
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.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.ChangedIdentity
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.None
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.UnsignedDevice
|
||||||
import io.element.android.features.messages.impl.timeline.components.MessageShieldView
|
import io.element.android.features.messages.impl.timeline.components.MessageShieldView
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||||
|
|
@ -90,6 +95,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 +132,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 +151,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 +166,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 +200,16 @@ private fun SheetContent(
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (target.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 +364,42 @@ private fun EmojiReactionsRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VerifiedUserSendFailureView(
|
||||||
|
sendFailure: VerifiedUserSendFailure,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
@Composable
|
||||||
|
fun VerifiedUserSendFailure.headline(): String {
|
||||||
|
return when (this) {
|
||||||
|
is None -> ""
|
||||||
|
is UnsignedDevice -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, userDisplayName)
|
||||||
|
is ChangedIdentity -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, userDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.headline(),
|
||||||
|
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 +449,6 @@ internal fun SheetContentPreview(
|
||||||
onActionClick = {},
|
onActionClick = {},
|
||||||
onEmojiReactionClick = {},
|
onEmojiReactionClick = {},
|
||||||
onCustomReactionClick = {},
|
onCustomReactionClick = {},
|
||||||
|
onVerifiedUserSendFailureClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface VerifiedUserSendFailure {
|
||||||
|
data object None : VerifiedUserSendFailure
|
||||||
|
|
||||||
|
data class UnsignedDevice(
|
||||||
|
val userDisplayName: String,
|
||||||
|
) : VerifiedUserSendFailure
|
||||||
|
|
||||||
|
data class ChangedIdentity(
|
||||||
|
val userDisplayName: String,
|
||||||
|
) : VerifiedUserSendFailure
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VerifiedUserSendFailureFactory @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
) {
|
||||||
|
suspend fun create(
|
||||||
|
sendState: LocalEventSendState?,
|
||||||
|
): VerifiedUserSendFailure {
|
||||||
|
return when (sendState) {
|
||||||
|
is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> {
|
||||||
|
val userId = sendState.devices.keys.firstOrNull()
|
||||||
|
if (userId == null) {
|
||||||
|
VerifiedUserSendFailure.None
|
||||||
|
} else {
|
||||||
|
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
|
||||||
|
VerifiedUserSendFailure.UnsignedDevice(displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> {
|
||||||
|
val userId = sendState.users.firstOrNull()
|
||||||
|
if (userId == null) {
|
||||||
|
VerifiedUserSendFailure.None
|
||||||
|
} else {
|
||||||
|
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
|
||||||
|
VerifiedUserSendFailure.ChangedIdentity(displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> VerifiedUserSendFailure.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
|
sealed interface ResolveVerifiedUserSendFailureEvents {
|
||||||
|
data class ComputeForMessage(
|
||||||
|
val messageEvent: TimelineItem.Event,
|
||||||
|
) : ResolveVerifiedUserSendFailureEvents
|
||||||
|
|
||||||
|
data object ResolveAndResend : ResolveVerifiedUserSendFailureEvents
|
||||||
|
data object Retry : ResolveVerifiedUserSendFailureEvents
|
||||||
|
data object Dismiss : ResolveVerifiedUserSendFailureEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ResolveVerifiedUserSendFailurePresenter @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val verifiedUserSendFailureFactory: VerifiedUserSendFailureFactory,
|
||||||
|
) : Presenter<ResolveVerifiedUserSendFailureState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): ResolveVerifiedUserSendFailureState {
|
||||||
|
var resolver by remember {
|
||||||
|
mutableStateOf<VerifiedUserSendFailureResolver?>(null)
|
||||||
|
}
|
||||||
|
val verifiedUserSendFailure by produceState<VerifiedUserSendFailure>(VerifiedUserSendFailure.None, resolver?.currentSendFailure?.value) {
|
||||||
|
val currentSendFailure = resolver?.currentSendFailure?.value
|
||||||
|
value = verifiedUserSendFailureFactory.create(currentSendFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolveAction = remember {
|
||||||
|
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
|
}
|
||||||
|
val retryAction = remember {
|
||||||
|
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
|
}
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
fun handleEvents(event: ResolveVerifiedUserSendFailureEvents) {
|
||||||
|
when (event) {
|
||||||
|
is ResolveVerifiedUserSendFailureEvents.ComputeForMessage -> {
|
||||||
|
val sendState = event.messageEvent.localSendState as? LocalEventSendState.Failed.VerifiedUser
|
||||||
|
val transactionId = event.messageEvent.transactionId
|
||||||
|
resolver = if (sendState != null && transactionId != null) {
|
||||||
|
VerifiedUserSendFailureResolver(
|
||||||
|
room = room,
|
||||||
|
transactionId = transactionId,
|
||||||
|
iterator = VerifiedUserSendFailureIterator.from(sendState)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResolveVerifiedUserSendFailureEvents.Dismiss -> {
|
||||||
|
resolver = null
|
||||||
|
}
|
||||||
|
ResolveVerifiedUserSendFailureEvents.Retry -> {
|
||||||
|
coroutineScope.launch {
|
||||||
|
resolver?.run {
|
||||||
|
runUpdatingState(retryAction) {
|
||||||
|
resend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResolveVerifiedUserSendFailureEvents.ResolveAndResend -> {
|
||||||
|
coroutineScope.launch {
|
||||||
|
resolver?.run {
|
||||||
|
runUpdatingState(resolveAction) {
|
||||||
|
resolveAndResend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResolveVerifiedUserSendFailureState(
|
||||||
|
verifiedUserSendFailure = verifiedUserSendFailure,
|
||||||
|
resolveAction = resolveAction.value,
|
||||||
|
retryAction = retryAction.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
|
data class ResolveVerifiedUserSendFailureState(
|
||||||
|
val verifiedUserSendFailure: VerifiedUserSendFailure,
|
||||||
|
val resolveAction: AsyncAction<Unit>,
|
||||||
|
val retryAction: AsyncAction<Unit>,
|
||||||
|
val eventSink: (ResolveVerifiedUserSendFailureEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
|
open class ResolveVerifiedUserSendFailureStateProvider : PreviewParameterProvider<ResolveVerifiedUserSendFailureState> {
|
||||||
|
override val values: Sequence<ResolveVerifiedUserSendFailureState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aResolveVerifiedUserSendFailureState(),
|
||||||
|
aResolveVerifiedUserSendFailureState(
|
||||||
|
verifiedUserSendFailure = anUnsignedDeviceSendFailure()
|
||||||
|
),
|
||||||
|
aResolveVerifiedUserSendFailureState(
|
||||||
|
verifiedUserSendFailure = aChangedIdentitySendFailure()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aResolveVerifiedUserSendFailureState(
|
||||||
|
verifiedUserSendFailure: VerifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
|
resolveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
retryAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
eventSink: (ResolveVerifiedUserSendFailureEvents) -> Unit = {}
|
||||||
|
) = ResolveVerifiedUserSendFailureState(
|
||||||
|
verifiedUserSendFailure = verifiedUserSendFailure,
|
||||||
|
resolveAction = resolveAction,
|
||||||
|
retryAction = retryAction,
|
||||||
|
eventSink = eventSink
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anUnsignedDeviceSendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.UnsignedDevice(
|
||||||
|
userDisplayName = userDisplayName,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aChangedIdentitySendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.ChangedIdentity(
|
||||||
|
userDisplayName = userDisplayName,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ResolveVerifiedUserSendFailureView(
|
||||||
|
state: ResolveVerifiedUserSendFailureState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
var showSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.Dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRetryClick() {
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResolveAndResendClick() {
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(state.verifiedUserSendFailure) {
|
||||||
|
if (state.verifiedUserSendFailure is VerifiedUserSendFailure.None) {
|
||||||
|
sheetState.hide()
|
||||||
|
showSheet = false
|
||||||
|
} else {
|
||||||
|
showSheet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
if (showSheet) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
modifier = Modifier
|
||||||
|
.systemBarsPadding()
|
||||||
|
.navigationBarsPadding(),
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = ::dismiss,
|
||||||
|
) {
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
title = state.verifiedUserSendFailure.title(),
|
||||||
|
subTitle = state.verifiedUserSendFailure.subtitle(),
|
||||||
|
iconImageVector = CompoundIcons.Error(),
|
||||||
|
iconTint = ElementTheme.colors.iconCriticalPrimary,
|
||||||
|
iconBackgroundTint = ElementTheme.colors.bgCriticalSubtle,
|
||||||
|
)
|
||||||
|
ButtonColumnMolecule(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = state.verifiedUserSendFailure.resolveAction(),
|
||||||
|
showProgress = state.resolveAction.isLoading(),
|
||||||
|
onClick = ::onResolveAndResendClick
|
||||||
|
)
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(id = CommonStrings.action_retry),
|
||||||
|
showProgress = state.retryAction.isLoading(),
|
||||||
|
onClick = ::onRetryClick
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(id = CommonStrings.action_cancel_for_now),
|
||||||
|
onClick = ::dismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VerifiedUserSendFailure.title(): String {
|
||||||
|
return when (this) {
|
||||||
|
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(id = CommonStrings.screen_resolve_send_failure_unsigned_device_title, userDisplayName)
|
||||||
|
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(
|
||||||
|
id = CommonStrings.screen_resolve_send_failure_changed_identity_title,
|
||||||
|
userDisplayName
|
||||||
|
)
|
||||||
|
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VerifiedUserSendFailure.subtitle(): String {
|
||||||
|
return when (this) {
|
||||||
|
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(
|
||||||
|
id = CommonStrings.screen_resolve_send_failure_unsigned_device_subtitle,
|
||||||
|
userDisplayName,
|
||||||
|
userDisplayName,
|
||||||
|
)
|
||||||
|
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(
|
||||||
|
id = CommonStrings.screen_resolve_send_failure_changed_identity_subtitle,
|
||||||
|
userDisplayName
|
||||||
|
)
|
||||||
|
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VerifiedUserSendFailure.resolveAction(): String {
|
||||||
|
return when (this) {
|
||||||
|
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(id = CommonStrings.screen_resolve_send_failure_unsigned_device_primary_button_title)
|
||||||
|
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(id = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title)
|
||||||
|
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ResolveVerifiedUserSendFailureViewPreview(
|
||||||
|
@PreviewParameter(ResolveVerifiedUserSendFailureStateProvider::class) state: ResolveVerifiedUserSendFailureState
|
||||||
|
) = ElementPreview {
|
||||||
|
ResolveVerifiedUserSendFailureView(state)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator for [LocalEventSendState.Failed.VerifiedUser]
|
||||||
|
* Allow to iterate through the internal state of the failure.
|
||||||
|
* This is useful to allow solving the failure step by step (e.g. for each user).
|
||||||
|
*/
|
||||||
|
interface VerifiedUserSendFailureIterator : Iterator<LocalEventSendState.Failed.VerifiedUser> {
|
||||||
|
companion object {
|
||||||
|
fun from(failure: LocalEventSendState.Failed.VerifiedUser): VerifiedUserSendFailureIterator {
|
||||||
|
return when (failure) {
|
||||||
|
is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> UnsignedDeviceSendFailureIterator(failure)
|
||||||
|
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> ChangedIdentitySendFailureIterator(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnsignedDeviceSendFailureIterator(
|
||||||
|
failure: LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice
|
||||||
|
) : VerifiedUserSendFailureIterator {
|
||||||
|
private val iterator = failure.devices.iterator()
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!hasNext()) {
|
||||||
|
Timber.w("Got $failure without any devices, shouldn't happen.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasNext(): Boolean {
|
||||||
|
return iterator.hasNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun next(): LocalEventSendState.Failed.VerifiedUser {
|
||||||
|
val (userId, deviceIds) = iterator.next()
|
||||||
|
return LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice(
|
||||||
|
mapOf(userId to deviceIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangedIdentitySendFailureIterator(
|
||||||
|
failure: LocalEventSendState.Failed.VerifiedUserChangedIdentity
|
||||||
|
) : VerifiedUserSendFailureIterator {
|
||||||
|
private val iterator = failure.users.iterator()
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!hasNext()) {
|
||||||
|
Timber.w("Got $failure without any users, shouldn't happen.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasNext(): Boolean {
|
||||||
|
return iterator.hasNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun next(): LocalEventSendState.Failed.VerifiedUser {
|
||||||
|
val userId = iterator.next()
|
||||||
|
return LocalEventSendState.Failed.VerifiedUserChangedIdentity(
|
||||||
|
listOf(userId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for resolving and resending a failed message sent to a verified user.
|
||||||
|
* It also allow to resend the message without resolving the failure, for example if the user has in the meantime verified their device again.
|
||||||
|
* It's using the [VerifiedUserSendFailureIterator] to iterate over the different failures (ie. the different users concerned by the failure).
|
||||||
|
* This way, the user can resolve and resend the message for each user concerned, one by one.
|
||||||
|
*/
|
||||||
|
class VerifiedUserSendFailureResolver(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val transactionId: TransactionId,
|
||||||
|
private val iterator: VerifiedUserSendFailureIterator,
|
||||||
|
) {
|
||||||
|
val currentSendFailure = mutableStateOf<LocalEventSendState.Failed.VerifiedUser?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
currentSendFailure.value = iterator.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resend(): Result<Unit> {
|
||||||
|
return room.retrySendMessage(transactionId)
|
||||||
|
.onSuccess {
|
||||||
|
Timber.d("Succeed to resend message with transactionId: $transactionId")
|
||||||
|
currentSendFailure.value = null
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Timber.e(it, "Failed to resend message with transactionId: $transactionId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resolveAndResend(): Result<Unit> {
|
||||||
|
return when (val failure = currentSendFailure.value) {
|
||||||
|
is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> {
|
||||||
|
room.ignoreDeviceTrustAndResend(failure.devices, transactionId)
|
||||||
|
}
|
||||||
|
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> {
|
||||||
|
room.withdrawVerificationAndResend(failure.users, transactionId)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Result.failure(IllegalStateException("Unknown send failure type"))
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
Timber.d("Succeed to resolve and resend message with transactionId: $transactionId")
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
val failure = iterator.next()
|
||||||
|
currentSendFailure.value = failure
|
||||||
|
} else {
|
||||||
|
currentSendFailure.value = null
|
||||||
|
Timber.d("No more failure to resolve for transactionId: $transactionId")
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it, "Failed to resolve and resend message with transactionId: $transactionId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,8 @@ package io.element.android.features.messages.impl.di
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -20,4 +22,7 @@ import io.element.android.libraries.di.RoomScope
|
||||||
interface MessagesModule {
|
interface MessagesModule {
|
||||||
@Binds
|
@Binds
|
||||||
fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter<PinnedMessagesBannerState>
|
fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter<PinnedMessagesBannerState>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindResolveVerifiedUserSendFailurePresenter(presenter: ResolveVerifiedUserSendFailurePresenter): Presenter<ResolveVerifiedUserSendFailureState>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ private fun PinnedMessagesListLoaded(
|
||||||
onSelectAction = ::onActionSelected,
|
onSelectAction = ::onActionSelected,
|
||||||
onCustomReactionClick = {},
|
onCustomReactionClick = {},
|
||||||
onEmojiReactionClick = { _, _ -> },
|
onEmojiReactionClick = { _, _ -> },
|
||||||
|
onVerifiedUserSendFailureClick = {}
|
||||||
)
|
)
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
|
|
@ -199,19 +200,18 @@ private fun PinnedMessagesListLoaded(
|
||||||
renderReadReceipts = false,
|
renderReadReceipts = false,
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
focusedEventId = null,
|
focusedEventId = null,
|
||||||
onClick = onEventClick,
|
|
||||||
onLongClick = ::onMessageLongClick,
|
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
onClick = onEventClick,
|
||||||
|
onLongClick = ::onMessageLongClick,
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
onReadReceiptClick = {},
|
onReadReceiptClick = {},
|
||||||
eventSink = {},
|
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
onShieldClick = {},
|
eventSink = {},
|
||||||
eventContentView = { event, contentModifier, onContentLayoutChange ->
|
eventContentView = { event, contentModifier, onContentLayoutChange ->
|
||||||
TimelineItemEventContentViewWrapper(
|
TimelineItemEventContentViewWrapper(
|
||||||
event = event,
|
event = event,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
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.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
|
@ -19,7 +20,6 @@ sealed interface TimelineEvents {
|
||||||
data object OnFocusEventRender : TimelineEvents
|
data object OnFocusEventRender : TimelineEvents
|
||||||
data object JumpToLive : TimelineEvents
|
data object JumpToLive : TimelineEvents
|
||||||
|
|
||||||
data class ShowShieldDialog(val messageShield: MessageShield) : TimelineEvents
|
|
||||||
data object HideShieldDialog : TimelineEvents
|
data object HideShieldDialog : TimelineEvents
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -27,6 +27,8 @@ sealed interface TimelineEvents {
|
||||||
*/
|
*/
|
||||||
sealed interface EventFromTimelineItem : TimelineEvents
|
sealed interface EventFromTimelineItem : TimelineEvents
|
||||||
|
|
||||||
|
data class ComputeVerifiedUserSendFailure(val event: TimelineItem.Event) : EventFromTimelineItem
|
||||||
|
data class ShowShieldDialog(val messageShield: MessageShield) : EventFromTimelineItem
|
||||||
data class LoadMore(val direction: Timeline.PaginationDirection) : EventFromTimelineItem
|
data class LoadMore(val direction: Timeline.PaginationDirection) : EventFromTimelineItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.impl.MessagesNavigator
|
import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureEvents
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
|
|
@ -66,6 +68,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val endPollAction: EndPollAction,
|
private val endPollAction: EndPollAction,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
private val timelineController: TimelineController,
|
private val timelineController: TimelineController,
|
||||||
|
private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -101,6 +104,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val newEventState = remember { mutableStateOf(NewEventState.None) }
|
val newEventState = remember { mutableStateOf(NewEventState.None) }
|
||||||
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
val resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailurePresenter.present()
|
||||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
val isLive by timelineController.isLive().collectAsState(initial = true)
|
val isLive by timelineController.isLive().collectAsState(initial = true)
|
||||||
|
|
@ -156,6 +160,9 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
TimelineEvents.HideShieldDialog -> messageShield.value = null
|
TimelineEvents.HideShieldDialog -> messageShield.value = null
|
||||||
is TimelineEvents.ShowShieldDialog -> messageShield.value = event.messageShield
|
is TimelineEvents.ShowShieldDialog -> messageShield.value = event.messageShield
|
||||||
|
is TimelineEvents.ComputeVerifiedUserSendFailure -> {
|
||||||
|
resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(event.event))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,6 +239,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
isLive = isLive,
|
isLive = isLive,
|
||||||
focusRequestState = focusRequestState.value,
|
focusRequestState = focusRequestState.value,
|
||||||
messageShield = messageShield.value,
|
messageShield = messageShield.value,
|
||||||
|
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
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.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -25,6 +26,7 @@ data class TimelineState(
|
||||||
val focusRequestState: FocusRequestState,
|
val focusRequestState: FocusRequestState,
|
||||||
// If not null, info will be rendered in a dialog
|
// If not null, info will be rendered in a dialog
|
||||||
val messageShield: MessageShield?,
|
val messageShield: MessageShield?,
|
||||||
|
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
|
||||||
val eventSink: (TimelineEvents) -> Unit,
|
val eventSink: (TimelineEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val hasAnyEvent = timelineItems.any { it is TimelineItem.Event }
|
val hasAnyEvent = timelineItems.any { it is TimelineItem.Event }
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
|
@ -44,6 +46,7 @@ fun aTimelineState(
|
||||||
focusedEventIndex: Int = -1,
|
focusedEventIndex: Int = -1,
|
||||||
isLive: Boolean = true,
|
isLive: Boolean = true,
|
||||||
messageShield: MessageShield? = null,
|
messageShield: MessageShield? = null,
|
||||||
|
resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(),
|
||||||
eventSink: (TimelineEvents) -> Unit = {},
|
eventSink: (TimelineEvents) -> Unit = {},
|
||||||
): TimelineState {
|
): TimelineState {
|
||||||
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
|
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
|
||||||
|
|
@ -60,6 +63,7 @@ fun aTimelineState(
|
||||||
isLive = isLive,
|
isLive = isLive,
|
||||||
focusRequestState = focusRequestState,
|
focusRequestState = focusRequestState,
|
||||||
messageShield = messageShield,
|
messageShield = messageShield,
|
||||||
|
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureView
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
|
||||||
import io.element.android.features.messages.impl.timeline.components.toText
|
import io.element.android.features.messages.impl.timeline.components.toText
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
|
|
@ -127,8 +128,8 @@ fun TimelineView(
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(nestedScrollConnection),
|
.nestedScroll(nestedScrollConnection),
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
reverseLayout = useReverseLayout,
|
reverseLayout = useReverseLayout,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
|
|
@ -150,19 +151,18 @@ fun TimelineView(
|
||||||
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true &&
|
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true &&
|
||||||
state.timelineItems.first().identifier() == timelineItem.identifier(),
|
state.timelineItems.first().identifier() == timelineItem.identifier(),
|
||||||
focusedEventId = state.focusedEventId,
|
focusedEventId = state.focusedEventId,
|
||||||
onClick = onMessageClick,
|
|
||||||
onLongClick = onMessageLongClick,
|
|
||||||
onShieldClick = ::onShieldClick,
|
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
onClick = onMessageClick,
|
||||||
|
onLongClick = onMessageLongClick,
|
||||||
inReplyToClick = ::inReplyToClick,
|
inReplyToClick = ::inReplyToClick,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
eventSink = state.eventSink,
|
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +186,8 @@ fun TimelineView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResolveVerifiedUserSendFailureView(state = state.resolveVerifiedUserSendFailureState)
|
||||||
|
|
||||||
MessageShieldDialog(state)
|
MessageShieldDialog(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,8 +269,8 @@ private fun BoxScope.TimelineScrollHelper(
|
||||||
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
||||||
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(end = 24.dp, bottom = 12.dp),
|
.padding(end = 24.dp, bottom = 12.dp),
|
||||||
onClick = { jumpToBottom() },
|
onClick = { jumpToBottom() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -295,8 +297,8 @@ private fun JumpToBottomButton(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.rotate(90f),
|
.rotate(90f),
|
||||||
imageVector = CompoundIcons.ArrowRight(),
|
imageVector = CompoundIcons.ArrowRight(),
|
||||||
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,8 @@ internal fun ATimelineItemEventRow(
|
||||||
isHighlighted = isHighlighted,
|
isHighlighted = isHighlighted,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
onShieldClick = {},
|
|
||||||
onUserDataClick = {},
|
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
|
onUserDataClick = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.isEdited
|
import io.element.android.features.messages.impl.timeline.model.event.isEdited
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
|
@ -31,14 +32,13 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
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.isCritical
|
import io.element.android.libraries.matrix.api.timeline.item.event.isCritical
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineEventTimestampView(
|
fun TimelineEventTimestampView(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
onShieldClick: (MessageShield) -> Unit,
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val formattedTime = event.sentTime
|
val formattedTime = event.sentTime
|
||||||
|
|
@ -48,8 +48,8 @@ fun TimelineEventTimestampView(
|
||||||
val tint = if (hasError || hasEncryptionCritical) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary
|
val tint = if (hasError || hasEncryptionCritical) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (isMessageEdited) {
|
if (isMessageEdited) {
|
||||||
|
|
@ -66,12 +66,17 @@ fun TimelineEventTimestampView(
|
||||||
color = tint,
|
color = tint,
|
||||||
)
|
)
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
|
val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Error(),
|
imageVector = CompoundIcons.Error(),
|
||||||
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
|
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
|
||||||
tint = tint,
|
tint = tint,
|
||||||
modifier = Modifier.size(15.dp, 18.dp),
|
modifier = Modifier
|
||||||
|
.size(15.dp, 18.dp)
|
||||||
|
.clickable(isVerifiedUserSendFailure) {
|
||||||
|
eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
event.messageShield?.let { shield ->
|
event.messageShield?.let { shield ->
|
||||||
|
|
@ -80,8 +85,10 @@ fun TimelineEventTimestampView(
|
||||||
imageVector = shield.toIcon(),
|
imageVector = shield.toIcon(),
|
||||||
contentDescription = shield.toText(),
|
contentDescription = shield.toText(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(15.dp)
|
.size(15.dp)
|
||||||
.clickable { onShieldClick(shield) },
|
.clickable {
|
||||||
|
eventSink(TimelineEvents.ShowShieldDialog(shield))
|
||||||
|
},
|
||||||
tint = shield.toIconColor(),
|
tint = shield.toIconColor(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
@ -94,7 +101,7 @@ fun TimelineEventTimestampView(
|
||||||
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
onShieldClick = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
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.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView
|
||||||
|
|
@ -110,7 +109,6 @@ fun TimelineItemEventRow(
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onShieldClick: (MessageShield) -> Unit,
|
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
|
@ -161,6 +159,17 @@ fun TimelineItemEventRow(
|
||||||
ReplySwipeIndicator({ offset / 120 })
|
ReplySwipeIndicator({ offset / 120 })
|
||||||
}
|
}
|
||||||
TimelineItemEventRowContent(
|
TimelineItemEventRowContent(
|
||||||
|
event = event,
|
||||||
|
isHighlighted = isHighlighted,
|
||||||
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
inReplyToClick = ::inReplyToClick,
|
||||||
|
onUserDataClick = ::onUserDataClick,
|
||||||
|
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
||||||
|
onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) },
|
||||||
|
onMoreReactionsClick = { onMoreReactionsClick(event) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.absoluteOffset { IntOffset(x = offset.roundToInt(), y = 0) }
|
.absoluteOffset { IntOffset(x = offset.roundToInt(), y = 0) }
|
||||||
.draggable(
|
.draggable(
|
||||||
|
|
@ -176,18 +185,7 @@ fun TimelineItemEventRow(
|
||||||
},
|
},
|
||||||
state = state.draggableState,
|
state = state.draggableState,
|
||||||
),
|
),
|
||||||
event = event,
|
eventSink = eventSink,
|
||||||
isHighlighted = isHighlighted,
|
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
onShieldClick = onShieldClick,
|
|
||||||
inReplyToClick = ::inReplyToClick,
|
|
||||||
onUserDataClick = ::onUserDataClick,
|
|
||||||
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
|
||||||
onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) },
|
|
||||||
onMoreReactionsClick = { onMoreReactionsClick(event) },
|
|
||||||
eventContentView = eventContentView,
|
eventContentView = eventContentView,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -200,12 +198,12 @@ fun TimelineItemEventRow(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onShieldClick = onShieldClick,
|
|
||||||
inReplyToClick = ::inReplyToClick,
|
inReplyToClick = ::inReplyToClick,
|
||||||
onUserDataClick = ::onUserDataClick,
|
onUserDataClick = ::onUserDataClick,
|
||||||
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
||||||
onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) },
|
onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) },
|
||||||
onMoreReactionsClick = { onMoreReactionsClick(event) },
|
onMoreReactionsClick = { onMoreReactionsClick(event) },
|
||||||
|
eventSink = eventSink,
|
||||||
eventContentView = eventContentView,
|
eventContentView = eventContentView,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -255,12 +253,12 @@ private fun TimelineItemEventRowContent(
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onShieldClick: (MessageShield) -> Unit,
|
|
||||||
inReplyToClick: () -> Unit,
|
inReplyToClick: () -> Unit,
|
||||||
onUserDataClick: () -> Unit,
|
onUserDataClick: () -> Unit,
|
||||||
onReactionClick: (emoji: String) -> Unit,
|
onReactionClick: (emoji: String) -> Unit,
|
||||||
onReactionLongClick: (emoji: String) -> Unit,
|
onReactionLongClick: (emoji: String) -> Unit,
|
||||||
onMoreReactionsClick: (event: TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (event: TimelineItem.Event) -> Unit,
|
||||||
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit,
|
eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -322,9 +320,9 @@ private fun TimelineItemEventRowContent(
|
||||||
) {
|
) {
|
||||||
MessageEventBubbleContent(
|
MessageEventBubbleContent(
|
||||||
event = event,
|
event = event,
|
||||||
onShieldClick = onShieldClick,
|
|
||||||
onMessageLongClick = onLongClick,
|
onMessageLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
|
eventSink = eventSink,
|
||||||
eventContentView = eventContentView,
|
eventContentView = eventContentView,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -382,9 +380,9 @@ private fun MessageSenderInformation(
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageEventBubbleContent(
|
private fun MessageEventBubbleContent(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
onShieldClick: (MessageShield) -> Unit,
|
|
||||||
onMessageLongClick: () -> Unit,
|
onMessageLongClick: () -> Unit,
|
||||||
inReplyToClick: () -> Unit,
|
inReplyToClick: () -> Unit,
|
||||||
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
@SuppressLint("ModifierParameter")
|
@SuppressLint("ModifierParameter")
|
||||||
// need to rename this modifier to prevent linter false positives
|
// need to rename this modifier to prevent linter false positives
|
||||||
@Suppress("ModifierNaming")
|
@Suppress("ModifierNaming")
|
||||||
|
|
@ -422,7 +420,7 @@ private fun MessageEventBubbleContent(
|
||||||
@Composable
|
@Composable
|
||||||
fun WithTimestampLayout(
|
fun WithTimestampLayout(
|
||||||
timestampPosition: TimestampPosition,
|
timestampPosition: TimestampPosition,
|
||||||
onShieldClick: (MessageShield) -> Unit,
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
canShrinkContent: Boolean = false,
|
canShrinkContent: Boolean = false,
|
||||||
content: @Composable (onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit) -> Unit,
|
content: @Composable (onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit) -> Unit,
|
||||||
|
|
@ -433,7 +431,7 @@ private fun MessageEventBubbleContent(
|
||||||
content {}
|
content {}
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
onShieldClick = onShieldClick,
|
eventSink = eventSink,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// Outer padding
|
// Outer padding
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||||
|
|
@ -454,7 +452,7 @@ private fun MessageEventBubbleContent(
|
||||||
overlay = {
|
overlay = {
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
onShieldClick = onShieldClick,
|
eventSink = eventSink,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -465,7 +463,7 @@ private fun MessageEventBubbleContent(
|
||||||
content {}
|
content {}
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
onShieldClick = onShieldClick,
|
eventSink = eventSink,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
|
@ -513,7 +511,7 @@ private fun MessageEventBubbleContent(
|
||||||
val contentWithTimestamp = @Composable {
|
val contentWithTimestamp = @Composable {
|
||||||
WithTimestampLayout(
|
WithTimestampLayout(
|
||||||
timestampPosition = timestampPosition,
|
timestampPosition = timestampPosition,
|
||||||
onShieldClick = onShieldClick,
|
eventSink = eventSink,
|
||||||
canShrinkContent = canShrinkContent,
|
canShrinkContent = canShrinkContent,
|
||||||
modifier = timestampLayoutModifier,
|
modifier = timestampLayoutModifier,
|
||||||
content = { onContentLayoutChange ->
|
content = { onContentLayoutChange ->
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
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.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemGroupedEventsRow(
|
fun TimelineItemGroupedEventsRow(
|
||||||
|
|
@ -40,7 +39,6 @@ fun TimelineItemGroupedEventsRow(
|
||||||
focusedEventId: EventId?,
|
focusedEventId: EventId?,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
onShieldClick: (MessageShield) -> Unit,
|
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
|
|
@ -77,7 +75,6 @@ fun TimelineItemGroupedEventsRow(
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onShieldClick = onShieldClick,
|
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
|
@ -102,7 +99,6 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
onShieldClick: (MessageShield) -> Unit,
|
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
|
|
@ -143,19 +139,18 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
focusedEventId = focusedEventId,
|
focusedEventId = focusedEventId,
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
onShieldClick = onShieldClick,
|
|
||||||
inReplyToClick = inReplyToClick,
|
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
inReplyToClick = inReplyToClick,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
eventSink = eventSink,
|
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
|
eventSink = eventSink,
|
||||||
eventContentView = eventContentView,
|
eventContentView = eventContentView,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +183,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
onShieldClick = {},
|
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
|
|
@ -213,7 +207,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
onShieldClick = {},
|
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.designsystem.text.toPx
|
||||||
import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor
|
import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor
|
||||||
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.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemRow(
|
internal fun TimelineItemRow(
|
||||||
|
|
@ -43,7 +42,6 @@ internal fun TimelineItemRow(
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
onShieldClick: (MessageShield) -> Unit,
|
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -115,9 +113,8 @@ internal fun TimelineItemRow(
|
||||||
isHighlighted = timelineItem.isEvent(focusedEventId),
|
isHighlighted = timelineItem.isEvent(focusedEventId),
|
||||||
onClick = { onClick(timelineItem) },
|
onClick = { onClick(timelineItem) },
|
||||||
onLongClick = { onLongClick(timelineItem) },
|
onLongClick = { onLongClick(timelineItem) },
|
||||||
onShieldClick = onShieldClick,
|
|
||||||
onUserDataClick = onUserDataClick,
|
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
onUserDataClick = onUserDataClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
|
@ -141,7 +138,6 @@ internal fun TimelineItemRow(
|
||||||
focusedEventId = focusedEventId,
|
focusedEventId = focusedEventId,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onShieldClick = onShieldClick,
|
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
|
||||||
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.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.draft.FakeComposerDraftService
|
import io.element.android.features.messages.impl.draft.FakeComposerDraftService
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
||||||
|
|
@ -1035,6 +1036,7 @@ class MessagesPresenterTest {
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
timelineItemIndexer = TimelineItemIndexer(),
|
timelineItemIndexer = TimelineItemIndexer(),
|
||||||
timelineController = TimelineController(matrixRoom),
|
timelineController = TimelineController(matrixRoom),
|
||||||
|
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
||||||
)
|
)
|
||||||
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
||||||
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.anActionListState
|
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||||
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.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aChangedIdentitySendFailure
|
||||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerItem
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerItem
|
||||||
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
||||||
|
|
@ -329,6 +331,7 @@ class MessagesViewTest {
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
actions = persistentListOf(TimelineItemAction.Edit),
|
actions = persistentListOf(TimelineItemAction.Edit),
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -399,6 +402,7 @@ class MessagesViewTest {
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(TimelineItemAction.Edit),
|
actions = persistentListOf(TimelineItemAction.Edit),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -416,6 +420,32 @@ class MessagesViewTest {
|
||||||
eventsRecorder.assertSingle(CustomReactionEvents.ShowCustomReactionSheet(timelineItem))
|
eventsRecorder.assertSingle(CustomReactionEvents.ShowCustomReactionSheet(timelineItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on verified user send failure from action list emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<TimelineEvents>()
|
||||||
|
val state = aMessagesState()
|
||||||
|
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||||
|
val stateWithActionListState = state.copy(
|
||||||
|
actionListState = anActionListState(
|
||||||
|
target = ActionListState.Target.Success(
|
||||||
|
event = timelineItem,
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = aChangedIdentitySendFailure(),
|
||||||
|
actions = persistentListOf(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
timelineState = aTimelineState(eventSink = eventsRecorder)
|
||||||
|
)
|
||||||
|
rule.setMessagesView(
|
||||||
|
state = stateWithActionListState,
|
||||||
|
)
|
||||||
|
val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice")
|
||||||
|
rule.onNodeWithText(verifiedUserSendFailure).performClick()
|
||||||
|
// Give time for the close animation to complete
|
||||||
|
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
|
||||||
|
eventsRecorder.assertSingle(TimelineEvents.ComputeVerifiedUserSendFailure(timelineItem))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on a custom emoji emits the expected Events`() {
|
fun `clicking on a custom emoji emits the expected Events`() {
|
||||||
val aUnicode = "🙈"
|
val aUnicode = "🙈"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.aUserEventPermissions
|
import io.element.android.features.messages.impl.aUserEventPermissions
|
||||||
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.actionlist.model.TimelineItemActionPostProcessor
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
||||||
|
|
@ -25,8 +27,10 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
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.features.poll.api.pollcontent.aPollAnswerItemList
|
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
||||||
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.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
|
|
@ -79,6 +83,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
|
|
@ -120,6 +125,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
|
|
@ -161,6 +167,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -208,6 +215,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
|
|
@ -252,6 +260,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -298,6 +307,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -345,6 +355,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -392,6 +403,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -439,6 +451,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -484,6 +497,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = stateEvent,
|
event = stateEvent,
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
|
|
@ -553,6 +567,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -599,6 +614,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -652,6 +668,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -748,6 +765,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
|
@ -787,6 +805,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
|
@ -829,6 +848,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
|
@ -870,6 +890,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
|
|
@ -910,6 +931,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
|
@ -949,6 +971,7 @@ class ActionListPresenterTest {
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ViewSource
|
TimelineItemAction.ViewSource
|
||||||
)
|
)
|
||||||
|
|
@ -956,6 +979,32 @@ class ActionListPresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for verified user send failure`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
userDisplayNameResult = { Result.success("Alice") }
|
||||||
|
)
|
||||||
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = false, room = room)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
sendState = LocalEventSendState.Failed.VerifiedUserChangedIdentity(users = listOf(A_USER_ID)),
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
userEventPermissions = aUserEventPermissions(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(1)
|
||||||
|
val successState = awaitItem()
|
||||||
|
val target = successState.target as ActionListState.Target.Success
|
||||||
|
assertThat(target.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(userDisplayName = "Alice"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createActionListPresenter(
|
private fun createActionListPresenter(
|
||||||
|
|
@ -968,6 +1017,7 @@ private fun createActionListPresenter(
|
||||||
postProcessor = TimelineItemActionPostProcessor.Default,
|
postProcessor = TimelineItemActionPostProcessor.Default,
|
||||||
appPreferencesStore = preferencesStore,
|
appPreferencesStore = preferencesStore,
|
||||||
isPinnedMessagesFeatureEnabled = { isPinFeatureEnabled },
|
isPinnedMessagesFeatureEnabled = { isPinFeatureEnabled },
|
||||||
room = room
|
room = room,
|
||||||
|
userSendFailureFactory = VerifiedUserSendFailureFactory(room)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,353 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
|
||||||
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
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.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ResolveVerifiedUserSendFailurePresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter()
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - remote message scenario`() = runTest {
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter()
|
||||||
|
presenter.test {
|
||||||
|
val sentMessage = aMessageEvent()
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(sentMessage))
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - sent message scenario`() = runTest {
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter()
|
||||||
|
presenter.test {
|
||||||
|
val sentMessage = aMessageEvent(
|
||||||
|
sendState = LocalEventSendState.Sent(AN_EVENT_ID)
|
||||||
|
)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(sentMessage))
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - unknown failed message scenario`() = runTest {
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter()
|
||||||
|
presenter.test {
|
||||||
|
val failedMessage = aMessageEvent(
|
||||||
|
sendState = LocalEventSendState.Failed.Unknown("")
|
||||||
|
)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - verified user unsigned device failure dismiss scenario`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
userDisplayNameResult = { userId ->
|
||||||
|
Result.success(userId.value)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter(room)
|
||||||
|
presenter.test {
|
||||||
|
val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage()
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.Dismiss)
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
}
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - verified user unsigned device failure retry scenario`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
userDisplayNameResult = { userId ->
|
||||||
|
Result.success(userId.value)
|
||||||
|
},
|
||||||
|
retrySendMessageResult = {
|
||||||
|
Result.success(Unit)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter(room)
|
||||||
|
presenter.test {
|
||||||
|
val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage()
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
|
||||||
|
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.Retry)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.retryAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
}
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
assertThat(state.retryAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
}
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - verified user unsigned device failure resolve and resend scenario`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
userDisplayNameResult = { userId ->
|
||||||
|
Result.success(userId.value)
|
||||||
|
},
|
||||||
|
ignoreDeviceTrustAndResendResult = { _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter(room)
|
||||||
|
presenter.test {
|
||||||
|
val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage()
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
|
||||||
|
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
}
|
||||||
|
// This should move to the next user
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID_2.value))
|
||||||
|
assertThat(state.resolveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
|
||||||
|
}
|
||||||
|
|
||||||
|
skipItems(3)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
}
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - verified user unsigned device failure resolve and resend scenario with error`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
userDisplayNameResult = { userId ->
|
||||||
|
Result.success(userId.value)
|
||||||
|
},
|
||||||
|
ignoreDeviceTrustAndResendResult = { _, _ ->
|
||||||
|
Result.failure(Exception())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter(room)
|
||||||
|
presenter.test {
|
||||||
|
val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage()
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
|
||||||
|
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
|
||||||
|
assertThat(state.resolveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
}
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - verified user changed identity failure retry scenario`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
userDisplayNameResult = { userId ->
|
||||||
|
Result.success(userId.value)
|
||||||
|
},
|
||||||
|
retrySendMessageResult = {
|
||||||
|
Result.success(Unit)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter(room)
|
||||||
|
presenter.test {
|
||||||
|
val failedMessage = aVerifiedUserChangedIdentityMessage()
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
|
||||||
|
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID.value))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.Retry)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.retryAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
}
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
assertThat(state.retryAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
}
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - verified user changed identity failure resolve and resend scenario`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
userDisplayNameResult = { userId ->
|
||||||
|
Result.success(userId.value)
|
||||||
|
},
|
||||||
|
withdrawVerificationAndResendResult = { _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter(room)
|
||||||
|
presenter.test {
|
||||||
|
val failedMessage = aVerifiedUserChangedIdentityMessage()
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
|
||||||
|
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID.value))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
}
|
||||||
|
// This should move to the next user
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID_2.value))
|
||||||
|
assertThat(state.resolveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
|
||||||
|
}
|
||||||
|
|
||||||
|
skipItems(3)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
}
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - verified user changed identity failure resolve and resend scenario with error`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
userDisplayNameResult = { userId ->
|
||||||
|
Result.success(userId.value)
|
||||||
|
},
|
||||||
|
withdrawVerificationAndResendResult = { _, _ ->
|
||||||
|
Result.failure(Exception())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val presenter = createResolveVerifiedUserSendFailurePresenter(room)
|
||||||
|
presenter.test {
|
||||||
|
val failedMessage = aVerifiedUserChangedIdentityMessage()
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None)
|
||||||
|
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
|
||||||
|
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID.value))
|
||||||
|
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID.value))
|
||||||
|
assertThat(state.resolveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
}
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aVerifiedUserHasUnsignedDeviceFailedMessage(): TimelineItem.Event {
|
||||||
|
return aMessageEvent(
|
||||||
|
transactionId = A_TRANSACTION_ID,
|
||||||
|
sendState = LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice(
|
||||||
|
mapOf(
|
||||||
|
A_USER_ID to emptyList(),
|
||||||
|
A_USER_ID_2 to emptyList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aVerifiedUserChangedIdentityMessage(): TimelineItem.Event {
|
||||||
|
return aMessageEvent(
|
||||||
|
transactionId = A_TRANSACTION_ID,
|
||||||
|
sendState = LocalEventSendState.Failed.VerifiedUserChangedIdentity(
|
||||||
|
listOf(A_USER_ID, A_USER_ID_2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createResolveVerifiedUserSendFailurePresenter(
|
||||||
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
): ResolveVerifiedUserSendFailurePresenter {
|
||||||
|
return ResolveVerifiedUserSendFailurePresenter(
|
||||||
|
room = room,
|
||||||
|
verifiedUserSendFailureFactory = VerifiedUserSendFailureFactory(room),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ResolveVerifiedUserSendFailureViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on resolve and resend emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ResolveVerifiedUserSendFailureEvents>()
|
||||||
|
rule.setResolveVerifiedUserSendFailureView(
|
||||||
|
state = aResolveVerifiedUserSendFailureState(
|
||||||
|
verifiedUserSendFailure = aChangedIdentitySendFailure(),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
rule.clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title)
|
||||||
|
eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on retry emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ResolveVerifiedUserSendFailureEvents>()
|
||||||
|
rule.setResolveVerifiedUserSendFailureView(
|
||||||
|
state = aResolveVerifiedUserSendFailureState(
|
||||||
|
verifiedUserSendFailure = aChangedIdentitySendFailure(),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
rule.clickOn(res = CommonStrings.action_retry)
|
||||||
|
eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvents.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResolveVerifiedUserSendFailureView(
|
||||||
|
state: ResolveVerifiedUserSendFailureState,
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
ResolveVerifiedUserSendFailureView(state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import app.cash.turbine.ReceiveTurbine
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.FakeMessagesNavigator
|
import io.element.android.features.messages.impl.FakeMessagesNavigator
|
||||||
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
||||||
import io.element.android.features.messages.impl.timeline.components.aCriticalShield
|
import io.element.android.features.messages.impl.timeline.components.aCriticalShield
|
||||||
|
|
@ -680,6 +681,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
timelineItemIndexer = timelineItemIndexer,
|
timelineItemIndexer = timelineItemIndexer,
|
||||||
timelineController = TimelineController(room),
|
timelineController = TimelineController(room),
|
||||||
|
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ fun aMatrixRoom(
|
||||||
emitRoomInfo: Boolean = false,
|
emitRoomInfo: Boolean = false,
|
||||||
canInviteResult: (UserId) -> Result<Boolean> = { lambdaError() },
|
canInviteResult: (UserId) -> Result<Boolean> = { lambdaError() },
|
||||||
canSendStateResult: (UserId, StateEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
|
canSendStateResult: (UserId, StateEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
|
||||||
userDisplayNameResult: () -> Result<String?> = { lambdaError() },
|
userDisplayNameResult: (UserId) -> Result<String?> = { lambdaError() },
|
||||||
userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
|
userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
|
||||||
setNameResult: (String) -> Result<Unit> = { lambdaError() },
|
setNameResult: (String) -> Result<Unit> = { lambdaError() },
|
||||||
setTopicResult: (String) -> Result<Unit> = { lambdaError() },
|
setTopicResult: (String) -> Result<Unit> = { lambdaError() },
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
||||||
* @param resourceId the resource id of the icon to display, exclusive with [imageVector]
|
* @param resourceId the resource id of the icon to display, exclusive with [imageVector]
|
||||||
* @param imageVector the image vector of the icon to display, exclusive with [resourceId]
|
* @param imageVector the image vector of the icon to display, exclusive with [resourceId]
|
||||||
* @param tint the tint to apply to the icon
|
* @param tint the tint to apply to the icon
|
||||||
|
* @param backgroundTint the tint to apply to the icon background
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun RoundedIconAtom(
|
fun RoundedIconAtom(
|
||||||
|
|
@ -44,13 +45,14 @@ fun RoundedIconAtom(
|
||||||
size: RoundedIconAtomSize = RoundedIconAtomSize.Large,
|
size: RoundedIconAtomSize = RoundedIconAtomSize.Large,
|
||||||
resourceId: Int? = null,
|
resourceId: Int? = null,
|
||||||
imageVector: ImageVector? = null,
|
imageVector: ImageVector? = null,
|
||||||
tint: Color = MaterialTheme.colorScheme.secondary
|
tint: Color = MaterialTheme.colorScheme.secondary,
|
||||||
|
backgroundTint: Color = ElementTheme.colors.temporaryColorBgSpecial,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(size.toContainerSize())
|
.size(size.toContainerSize())
|
||||||
.background(
|
.background(
|
||||||
color = ElementTheme.colors.temporaryColorBgSpecial,
|
color = backgroundTint,
|
||||||
shape = RoundedCornerShape(size.toCornerSize())
|
shape = RoundedCornerShape(size.toCornerSize())
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IconTitleSubtitleMolecule is a molecule which displays an icon, a title and a subtitle.
|
* IconTitleSubtitleMolecule is a molecule which displays an icon, a title and a subtitle.
|
||||||
|
|
@ -37,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
* @param iconResourceId the resource id of the icon to display, exclusive with [iconImageVector]
|
* @param iconResourceId the resource id of the icon to display, exclusive with [iconImageVector]
|
||||||
* @param iconImageVector the image vector of the icon to display, exclusive with [iconResourceId]
|
* @param iconImageVector the image vector of the icon to display, exclusive with [iconResourceId]
|
||||||
* @param iconTint the tint to apply to the icon
|
* @param iconTint the tint to apply to the icon
|
||||||
|
* @param iconBackgroundTint the tint to apply to the icon background
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun IconTitleSubtitleMolecule(
|
fun IconTitleSubtitleMolecule(
|
||||||
|
|
@ -46,6 +48,7 @@ fun IconTitleSubtitleMolecule(
|
||||||
iconResourceId: Int? = null,
|
iconResourceId: Int? = null,
|
||||||
iconImageVector: ImageVector? = null,
|
iconImageVector: ImageVector? = null,
|
||||||
iconTint: Color = MaterialTheme.colorScheme.primary,
|
iconTint: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
iconBackgroundTint: Color = ElementTheme.colors.temporaryColorBgSpecial,
|
||||||
) {
|
) {
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
RoundedIconAtom(
|
RoundedIconAtom(
|
||||||
|
|
@ -55,6 +58,7 @@ fun IconTitleSubtitleMolecule(
|
||||||
resourceId = iconResourceId,
|
resourceId = iconResourceId,
|
||||||
imageVector = iconImageVector,
|
imageVector = iconImageVector,
|
||||||
tint = iconTint,
|
tint = iconTint,
|
||||||
|
backgroundTint = iconBackgroundTint,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,41 @@ fun ListItem(
|
||||||
disabledLeadingIconColor = ListItemDefaultColors.iconDisabled,
|
disabledLeadingIconColor = ListItemDefaultColors.iconDisabled,
|
||||||
disabledTrailingIconColor = ListItemDefaultColors.iconDisabled,
|
disabledTrailingIconColor = ListItemDefaultColors.iconDisabled,
|
||||||
)
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
modifier = modifier,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
colors = colors,
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A List Item component to be used in lists and menus with simple layouts, matching the Material 3 guidelines.
|
||||||
|
* @param headlineContent The main content of the list item, usually a text.
|
||||||
|
* @param colors The colors to use for the list item. You can use [ListItemDefaults.colors] to create this.
|
||||||
|
* @param modifier The modifier to be applied to the list item.
|
||||||
|
* @param supportingContent The content to be displayed below the headline content.
|
||||||
|
* @param leadingContent The content to be displayed before the headline content.
|
||||||
|
* @param trailingContent The content to be displayed after the headline content.
|
||||||
|
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
|
||||||
|
* @param onClick The callback to be called when the list item is clicked.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun ListItem(
|
||||||
|
headlineContent: @Composable () -> Unit,
|
||||||
|
colors: ListItemColors,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
supportingContent: @Composable (() -> Unit)? = null,
|
||||||
|
leadingContent: ListItemContent? = null,
|
||||||
|
trailingContent: ListItemContent? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
|
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
|
||||||
val headlineColor = if (enabled) colors.headlineColor else colors.disabledHeadlineColor
|
val headlineColor = if (enabled) colors.headlineColor else colors.disabledHeadlineColor
|
||||||
val leadingContentColor = if (enabled) colors.leadingIconColor else colors.disabledLeadingIconColor
|
val leadingContentColor = if (enabled) colors.leadingIconColor else colors.disabledLeadingIconColor
|
||||||
|
|
@ -378,8 +412,8 @@ private object PreviewItems {
|
||||||
) {
|
) {
|
||||||
ElementThemedPreview {
|
ElementThemedPreview {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = PreviewItems.headline(),
|
headlineContent = headline(),
|
||||||
supportingContent = PreviewItems.text(),
|
supportingContent = text(),
|
||||||
leadingContent = leadingContent,
|
leadingContent = leadingContent,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
style = style,
|
style = style,
|
||||||
|
|
@ -397,8 +431,8 @@ private object PreviewItems {
|
||||||
) {
|
) {
|
||||||
ElementThemedPreview {
|
ElementThemedPreview {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = PreviewItems.headline(),
|
headlineContent = headline(),
|
||||||
supportingContent = PreviewItems.textSingleLine(),
|
supportingContent = textSingleLine(),
|
||||||
leadingContent = leadingContent,
|
leadingContent = leadingContent,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
style = style,
|
style = style,
|
||||||
|
|
@ -417,7 +451,7 @@ private object PreviewItems {
|
||||||
) {
|
) {
|
||||||
ElementThemedPreview {
|
ElementThemedPreview {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = PreviewItems.headline(),
|
headlineContent = headline(),
|
||||||
leadingContent = leadingContent,
|
leadingContent = leadingContent,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.room
|
package io.element.android.libraries.matrix.api.room
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
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.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
|
@ -349,5 +350,24 @@ interface MatrixRoom : Closeable {
|
||||||
*/
|
*/
|
||||||
suspend fun clearComposerDraft(): Result<Unit>
|
suspend fun clearComposerDraft(): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ignore the local trust for the given devices and resend messages that failed to send because said devices are unverified.
|
||||||
|
*
|
||||||
|
* @param devices The map of users identifiers to device identifiers received in the error
|
||||||
|
* @param transactionId The send queue transaction identifier of the local echo the send error applies to.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, transactionId: TransactionId): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove verification requirements for the given users and
|
||||||
|
* resend messages that failed to send because their identities were no longer verified.
|
||||||
|
*
|
||||||
|
* @param userIds The list of users identifiers received in the error.
|
||||||
|
* @param transactionId The send queue transaction identifier of the local echo the send error applies to.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
suspend fun withdrawVerificationAndResend(userIds: List<UserId>, transactionId: TransactionId): Result<Unit>
|
||||||
|
|
||||||
override fun close() = destroy()
|
override fun close() = destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.libraries.matrix.api.timeline.item.event
|
package io.element.android.libraries.matrix.api.timeline.item.event
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
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.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
|
@ -18,21 +19,24 @@ sealed interface LocalEventSendState {
|
||||||
data class Unknown(val error: String) : Failed
|
data class Unknown(val error: String) : Failed
|
||||||
data object CrossSigningNotSetup : Failed
|
data object CrossSigningNotSetup : Failed
|
||||||
data object SendingFromUnverifiedDevice : Failed
|
data object SendingFromUnverifiedDevice : Failed
|
||||||
|
|
||||||
|
sealed interface VerifiedUser : Failed
|
||||||
data class VerifiedUserHasUnsignedDevice(
|
data class VerifiedUserHasUnsignedDevice(
|
||||||
/**
|
/**
|
||||||
* The unsigned devices belonging to verified users. A map from user ID
|
* The unsigned devices belonging to verified users. A map from user ID
|
||||||
* to a list of device IDs.
|
* to a list of device IDs.
|
||||||
*/
|
*/
|
||||||
val devices: Map<UserId, List<String>>
|
val devices: Map<UserId, List<DeviceId>>
|
||||||
) : Failed
|
) : VerifiedUser
|
||||||
|
|
||||||
data class VerifiedUserChangedIdentity(
|
data class VerifiedUserChangedIdentity(
|
||||||
/**
|
/**
|
||||||
* The users that were previously verified but are no longer.
|
* The users that were previously verified but are no longer.
|
||||||
*/
|
*/
|
||||||
val users: List<UserId>
|
val users: List<UserId>
|
||||||
) : Failed
|
) : VerifiedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Sent(
|
data class Sent(
|
||||||
val eventId: EventId
|
val eventId: EventId
|
||||||
) : LocalEventSendState
|
) : LocalEventSendState
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ class RustMatrixClientFactory @Inject constructor(
|
||||||
strategy = if (featureFlagService.isFeatureEnabled(FeatureFlags.InvisibleCrypto)) {
|
strategy = if (featureFlagService.isFeatureEnabled(FeatureFlags.InvisibleCrypto)) {
|
||||||
CollectStrategy.IdentityBasedStrategy
|
CollectStrategy.IdentityBasedStrategy
|
||||||
} else {
|
} else {
|
||||||
CollectStrategy.DeviceBasedStrategy(onlyAllowTrustedDevices = false, errorOnVerifiedUserProblem = false)
|
CollectStrategy.DeviceBasedStrategy(onlyAllowTrustedDevices = false, errorOnVerifiedUserProblem = true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.run {
|
.run {
|
||||||
|
|
|
||||||
|
|
@ -459,8 +459,8 @@ class RustMatrixRoom(
|
||||||
return liveTimeline.forwardEvent(eventId, roomIds)
|
return liveTimeline.forwardEvent(eventId, roomIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> {
|
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = runCatching {
|
||||||
return Result.failure(UnsupportedOperationException("Not supported"))
|
innerRoom.tryResend(transactionId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
|
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
|
||||||
|
|
@ -645,6 +645,22 @@ class RustMatrixRoom(
|
||||||
innerRoom.clearComposerDraft()
|
innerRoom.clearComposerDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, transactionId: TransactionId) = runCatching {
|
||||||
|
innerRoom.ignoreDeviceTrustAndResend(
|
||||||
|
devices = devices.entries.associate { entry ->
|
||||||
|
entry.key.value to entry.value.map { it.value }
|
||||||
|
},
|
||||||
|
transactionId = transactionId.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun withdrawVerificationAndResend(userIds: List<UserId>, transactionId: TransactionId) = runCatching {
|
||||||
|
innerRoom.withdrawVerificationAndResend(
|
||||||
|
userIds = userIds.map { it.value },
|
||||||
|
transactionId = transactionId.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun createTimeline(
|
private fun createTimeline(
|
||||||
timeline: InnerTimeline,
|
timeline: InnerTimeline,
|
||||||
mode: Timeline.Mode,
|
mode: Timeline.Mode,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.timeline.item.event
|
package io.element.android.libraries.matrix.impl.timeline.item.event
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
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.core.TransactionId
|
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -87,7 +88,9 @@ fun RustEventSendState?.map(): LocalEventSendState? {
|
||||||
}
|
}
|
||||||
is RustEventSendState.VerifiedUserHasUnsignedDevice -> {
|
is RustEventSendState.VerifiedUserHasUnsignedDevice -> {
|
||||||
LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice(
|
LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice(
|
||||||
devices = devices.mapKeys { UserId(it.key) }
|
devices = devices.entries.associate { entry ->
|
||||||
|
UserId(entry.key) to entry.value.map { DeviceId(it) }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
EventSendState.CrossSigningNotSetup -> LocalEventSendState.Failed.CrossSigningNotSetup
|
EventSendState.CrossSigningNotSetup -> LocalEventSendState.Failed.CrossSigningNotSetup
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.test.room
|
package io.element.android.libraries.matrix.test.room
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
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.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
|
@ -79,7 +80,7 @@ class FakeMatrixRoom(
|
||||||
private var roomPermalinkResult: () -> Result<String> = { lambdaError() },
|
private var roomPermalinkResult: () -> Result<String> = { lambdaError() },
|
||||||
private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() },
|
private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() },
|
||||||
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() },
|
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() },
|
||||||
private val userDisplayNameResult: () -> Result<String?> = { lambdaError() },
|
private val userDisplayNameResult: (UserId) -> Result<String?> = { lambdaError() },
|
||||||
private val userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
|
private val userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
|
||||||
private val userRoleResult: () -> Result<RoomMember.Role> = { lambdaError() },
|
private val userRoleResult: () -> Result<RoomMember.Role> = { lambdaError() },
|
||||||
private val getUpdatedMemberResult: (UserId) -> Result<RoomMember> = { lambdaError() },
|
private val getUpdatedMemberResult: (UserId) -> Result<RoomMember> = { lambdaError() },
|
||||||
|
|
@ -134,7 +135,9 @@ class FakeMatrixRoom(
|
||||||
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
|
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
|
||||||
private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) },
|
private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) },
|
||||||
private val subscribeToSyncLambda: () -> Unit = { lambdaError() },
|
private val subscribeToSyncLambda: () -> Unit = { lambdaError() },
|
||||||
) : MatrixRoom {
|
private val ignoreDeviceTrustAndResendResult: (Map<UserId, List<DeviceId>>, TransactionId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
|
private val withdrawVerificationAndResendResult: (List<UserId>, TransactionId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
|
) : MatrixRoom {
|
||||||
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
|
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
|
||||||
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
|
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
|
||||||
|
|
||||||
|
|
@ -199,7 +202,7 @@ class FakeMatrixRoom(
|
||||||
override fun destroy() = Unit
|
override fun destroy() = Unit
|
||||||
|
|
||||||
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
|
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
|
||||||
userDisplayNameResult()
|
userDisplayNameResult(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = simulateLongTask {
|
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = simulateLongTask {
|
||||||
|
|
@ -226,7 +229,7 @@ class FakeMatrixRoom(
|
||||||
return toggleReactionResult(emoji, uniqueId)
|
return toggleReactionResult(emoji, uniqueId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> {
|
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = simulateLongTask {
|
||||||
return retrySendMessageResult(transactionId)
|
return retrySendMessageResult(transactionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -492,6 +495,14 @@ class FakeMatrixRoom(
|
||||||
return getWidgetDriverResult(widgetSettings)
|
return getWidgetDriverResult(widgetSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, transactionId: TransactionId): Result<Unit> = simulateLongTask {
|
||||||
|
return ignoreDeviceTrustAndResendResult(devices, transactionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun withdrawVerificationAndResend(userIds: List<UserId>, transactionId: TransactionId): Result<Unit> = simulateLongTask {
|
||||||
|
return withdrawVerificationAndResendResult(userIds, transactionId)
|
||||||
|
}
|
||||||
|
|
||||||
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
||||||
membersStateFlow.value = state
|
membersStateFlow.value = state
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:dccf918b02e10cc95f48de10ed7fbbf1d09fed2629d73ec09308a6604c02c0e3
|
||||||
|
size 47777
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fa076d0959c240108d948841358a5f485f3ab53d470de58c9c81e6d0b32cbd43
|
||||||
|
size 46843
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
|
||||||
|
size 3642
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ad9fa358acfb3257129c6ad0966af876cb469bab06969bef1e56b286ce99fe8b
|
||||||
|
size 58822
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4a6261c5a443005e4cb8f3e8f8f176c4e367feca6d6c1eb89ecc6b72aac119d3
|
||||||
|
size 55135
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
|
||||||
|
size 3659
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5704cb8201901bcb0a4a3a28a21390133fe1010fb928cae94530a2900f01e336
|
||||||
|
size 57172
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:423d72e2dab1e87c9ee52b170c730e189cb642e22d218cf4b2c7a99783cdda5f
|
||||||
|
size 53333
|
||||||
Loading…
Add table
Add a link
Reference in a new issue