diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt index 3f01f8337e..177807d501 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt @@ -41,7 +41,7 @@ class SendQueues( fun launchIn(coroutineScope: CoroutineScope) { combine( syncService.syncState, - matrixClient.sendQueueDisabledFlow(), + matrixClient.sendQueueDisabledFlow().onEach { Timber.tag("SendQueues").d("Send queue disabled for room: $it") }, ) { syncState, _ -> syncState } .debounce(SEND_QUEUES_RETRY_DELAY_MILLIS) .onEach { syncState -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 36d5dcaae9..5ebdd4ef19 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -86,6 +86,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.SendTarget import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.ui.messages.reply.map @@ -441,14 +442,13 @@ class MessagesPresenter( TimelineItemAction.Unpin -> handleUnpinAction(targetEvent) TimelineItemAction.ViewInTimeline -> Unit TimelineItemAction.RetrySending -> handleRetrySending(targetEvent) + TimelineItemAction.AbortSending -> handleActionAbortSend(targetEvent) } } private suspend fun handleRetrySending(targetEvent: TimelineItem.Event) { - val sendHandle = targetEvent.sendhandle ?: return Unit.also { - Timber.w("No send handle for event ${targetEvent.eventOrTransactionId}") - } - sendHandle.retry() + val (sendTarget, _) = targetEvent.pendingSend() ?: return + timelineController.retrySend(targetEvent.eventOrTransactionId, sendTarget) .onSuccess { Timber.d("Succeed to add the message back to the send queue") } @@ -538,13 +538,18 @@ class MessagesPresenter( private suspend fun handleActionRedact(event: TimelineItem.Event, redactEventAction: MutableState>) { val eventId = event.eventId if (eventId == null) { - // The message was never sent, so there is nobody to give a reason to. - redact(event.eventOrTransactionId, reason = null) + // The message (or its edit/redaction) was never sent, just remove it from the send queue. + handleActionAbortSend(event) } else { redactEventAction.value = MessagesState.ConfirmingRedaction(eventId) } } + private suspend fun handleActionAbortSend(event: TimelineItem.Event) { + val sendTarget = event.pendingSend()?.first ?: SendTarget.Event + timelineController.abortSend(event.eventOrTransactionId, sendTarget).onFailure { Timber.e(it) } + } + private suspend fun redact(eventOrTransactionId: EventOrTransactionId, reason: String?) { timelineController.invokeOnCurrentTimeline { redactEvent(eventOrTransactionId = eventOrTransactionId, reason = reason) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 50b90be650..e5c312132d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -486,7 +486,7 @@ fun MessagesView( onRemoveMessage = { event -> state.eventSink( MessagesEvent.HandleAction( - action = TimelineItemAction.Redact, + action = TimelineItemAction.AbortSending, event = event, ) ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index 3c64223e59..e78b2ac5e2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -37,6 +37,7 @@ enum class TimelineItemAction( Pin(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin), Unpin(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin), - // Note: this action is not used in UI for now + // Note: these actions are not used in UI for now RetrySending(CommonStrings.action_retry, CompoundDrawables.ic_compound_send_solid), + AbortSending(CommonStrings.action_cancel, CompoundDrawables.ic_compound_send_solid), } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt index 2f6fa4e4e0..3f0abf1b8d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt @@ -32,8 +32,9 @@ class TimelineItemActionComparator : Comparator { TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, - // Not rendered in the action list, but has to be listed here for exhaustiveness. + // Not rendered in the action list, but have to be listed here for exhaustiveness. TimelineItemAction.RetrySending, + TimelineItemAction.AbortSending, ) override fun compare(o1: TimelineItemAction, o2: TimelineItemAction): Int { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index 05f7a06054..b2639d08c6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -23,6 +23,8 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider +import io.element.android.libraries.matrix.api.timeline.item.SendTarget +import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -32,6 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.getAndUpdate @@ -134,6 +137,14 @@ class TimelineController( } } + suspend fun retrySend(eventOrTransactionId: EventOrTransactionId, sendTarget: SendTarget): Result { + return currentTimelineFlow.first().retrySend(eventOrTransactionId, sendTarget) + } + + suspend fun abortSend(eventOrTransactionId: EventOrTransactionId, sendTarget: SendTarget): Result { + return currentTimelineFlow.first().abortSend(eventOrTransactionId, sendTarget) + } + private val currentTimelineFlow = combine(liveTimelineFlow, detachedTimelineFlow) { live, detached -> when { detached.isPresent -> detached.get() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index a7006985a5..1888864311 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -302,7 +302,9 @@ class TimelinePresenter( } is TimelineEvent.ShowShieldDialog -> messageShieldDialogData.value = event.messageShieldData is TimelineEvent.ShowSendFailureDialog -> { - val sendStateFailure = event.event.localSendState as? LocalEventSendState.Failed ?: return + val (_, sendStateFailure) = event.event.pendingSend() ?: return + if (sendStateFailure !is LocalEventSendState.Failed) return + when (sendStateFailure) { is LocalEventSendState.Failed.VerifiedUser -> { // defer to the resolveVerifiedUserSendFailureState to handle this case, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 7896c1fcad..5f9f56a910 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -159,6 +159,8 @@ internal fun aTimelineItemEvent( content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, sendState: LocalEventSendState? = null, + editSendState: LocalEventSendState? = null, + redactionSendState: LocalEventSendState? = null, inReplyTo: InReplyToDetails? = null, threadInfo: TimelineItemThreadInfo? = null, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), @@ -185,6 +187,8 @@ internal fun aTimelineItemEvent( ), groupPosition = groupPosition, localSendState = sendState, + editSendState = editSendState, + redactionSendState = redactionSendState, inReplyTo = inReplyTo, threadInfo = threadInfo, origin = null, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index d7492ca40b..7bc5cbf059 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -55,13 +55,20 @@ fun TimelineEventTimestampView( val shield = event.messageShield val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser val onClickLabel = when { + hasError -> stringResource(CommonStrings.a11y_view_details) shield != null -> stringResource(CommonStrings.a11y_view_details) isVerifiedUserSendFailure -> stringResource(CommonStrings.action_open_context_menu) - hasError -> stringResource(CommonStrings.a11y_view_details) else -> null } val clickableModifier = remember(event) { when { + hasError -> { + Modifier.clickable( + onClickLabel = onClickLabel, + ) { + eventSink(TimelineEvent.ShowSendFailureDialog(event)) + } + } shield != null -> { Modifier.clickable( onClickLabel = onClickLabel, @@ -69,12 +76,6 @@ fun TimelineEventTimestampView( eventSink(TimelineEvent.ShowShieldDialog(shield)) } } - hasError -> Modifier - .clickable( - onClickLabel = onClickLabel, - ) { - eventSink(TimelineEvent.ShowSendFailureDialog(event)) - } else -> Modifier } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 5d77a7ada3..2dfea52e4c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -324,9 +324,10 @@ fun TimelineItemEventRow( } // Read receipts / Send state + val sendState = event.pendingSend()?.second TimelineItemReadReceiptView( state = ReadReceiptViewState( - sendState = event.localSendState, + sendState = sendState, isLastOutgoingMessage = isLastOutgoingMessage, receipts = event.readReceiptState.receipts, ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStatePreviewParam.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStatePreviewParam.kt index 751a88cb39..0810a41dce 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStatePreviewParam.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStatePreviewParam.kt @@ -20,9 +20,9 @@ import kotlinx.collections.immutable.toImmutableList class ReadReceiptViewStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aReadReceiptViewState(), - aReadReceiptViewState(sendState = LocalEventSendState.Sending.Event), - aReadReceiptViewState(sendState = LocalEventSendState.Sent(EventId("\$eventId"))), + aReadReceiptViewState(sendState = null, isLastOutgoingMessage = true), + aReadReceiptViewState(sendState = LocalEventSendState.Sending.Event, isLastOutgoingMessage = true), + aReadReceiptViewState(sendState = LocalEventSendState.Sent(EventId("\$eventId")), isLastOutgoingMessage = true), aReadReceiptViewState( sendState = LocalEventSendState.Sent(EventId("\$eventId")), receipts = List(1) { aReadReceiptData(it) }, @@ -43,12 +43,15 @@ class ReadReceiptViewStatePreviewParam : PreviewParameterProvider = emptyList(), ) = ReadReceiptViewState( sendState = sendState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index 17a9b0c5eb..30b4ee2d42 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -57,43 +57,60 @@ fun TimelineItemReadReceiptView( onReadReceiptsClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (state.receipts.isNotEmpty()) { - ReadReceiptsRow(modifier = modifier) { - ReadReceiptsAvatars( - receipts = state.receipts, - onClick = onReadReceiptsClick, - ) + val content: @Composable (() -> Unit)? = when (state.sendState) { + // If null, assume it's Sent and act the same way + null if state.isLastOutgoingMessage && state.receipts.isEmpty() -> { + @Composable { SentIcon() } } - } else { - when (state.sendState) { - is LocalEventSendState.Sending -> { - ReadReceiptsRow(modifier) { - Icon( - modifier = Modifier.padding(2.dp), - imageVector = CompoundIcons.Circle(), - contentDescription = stringResource(id = CommonStrings.common_sending), - tint = ElementTheme.colors.iconSecondary - ) - } - } - is LocalEventSendState.Failed -> { - // Error? The timestamp is already displayed in red - } - null, - is LocalEventSendState.Sent -> { - if (state.isLastOutgoingMessage) { - ReadReceiptsRow(modifier = modifier) { - Icon( - modifier = Modifier.padding(2.dp), - imageVector = CompoundIcons.CheckCircle(), - contentDescription = stringResource(id = CommonStrings.common_sent), - tint = ElementTheme.colors.iconSecondary - ) - } - } + is LocalEventSendState.Sending -> { + @Composable { SendingIcon() } + } + is LocalEventSendState.Failed -> { + // Error? The timestamp is already displayed in red + null + } + is LocalEventSendState.Sent if state.isLastOutgoingMessage && state.receipts.isEmpty() -> { + @Composable { SentIcon() } + } + else -> if (state.receipts.isNotEmpty()) { + @Composable { + ReadReceiptsAvatars( + receipts = state.receipts, + onClick = onReadReceiptsClick, + ) } + } else { + null } } + + // Only show the row if there is content to display + content?.let { + ReadReceiptsRow( + modifier = modifier, + content = it, + ) + } +} + +@Composable +private fun SendingIcon() { + Icon( + modifier = Modifier.padding(2.dp), + imageVector = CompoundIcons.Circle(), + contentDescription = stringResource(id = CommonStrings.common_sending), + tint = ElementTheme.colors.iconSecondary + ) +} + +@Composable +private fun SentIcon() { + Icon( + modifier = Modifier.padding(2.dp), + imageVector = CompoundIcons.CheckCircle(), + contentDescription = stringResource(id = CommonStrings.common_sent), + tint = ElementTheme.colors.iconSecondary + ) } @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStatePreviewParam.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStatePreviewParam.kt index 86c2beb707..9577ba297a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStatePreviewParam.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStatePreviewParam.kt @@ -19,7 +19,7 @@ class ReadReceiptBottomSheetStatePreviewParam : PreviewParameterProvider = readReceiptViewStateProvider.values - .filter { it.sendState is LocalEventSendState.Sent } + .filter { it.sendState is LocalEventSendState.Sent && it.receipts.isNotEmpty() } .map { readReceiptViewState -> ReadReceiptBottomSheetState( selectedEvent = aTimelineItemEvent( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index 6cd6b66cf1..b7b072880e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -123,6 +123,8 @@ class TimelineItemEventFactory( reactionsState = currentTimelineItem.computeReactionsState(), readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers, renderReadReceipts), localSendState = currentTimelineItem.event.localSendState, + editSendState = currentTimelineItem.event.editSendState, + redactionSendState = currentTimelineItem.event.redactionSendState, inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser), threadInfo = mappedThreadInfo, origin = currentTimelineItem.event.origin, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 1cbc2ec29c..8a108b31f9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.SendTarget import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -92,6 +93,8 @@ sealed interface TimelineItem { val reactionsState: TimelineItemReactions, val readReceiptState: TimelineItemReadReceipts, val localSendState: LocalEventSendState?, + private val editSendState: LocalEventSendState?, + private val redactionSendState: LocalEventSendState?, val inReplyTo: InReplyToDetails?, val threadInfo: TimelineItemThreadInfo?, val origin: TimelineItemEventOrigin?, @@ -110,7 +113,7 @@ sealed interface TimelineItem { val safeSenderName: String = senderProfile.getDisambiguatedDisplayName(senderId) - val failedToSend: Boolean = localSendState is LocalEventSendState.Failed + val failedToSend: Boolean = listOf(localSendState, editSendState, redactionSendState).any { it is LocalEventSendState.Failed } val isTextMessage: Boolean = content is TimelineItemTextBasedContent @@ -143,6 +146,18 @@ sealed interface TimelineItem { get() = timelineItemDebugInfoProvider() val sendhandle: SendHandle? get() = sendHandleProvider() + + /** + * Returns the first pending send state, if any, along with the corresponding [SendTarget]. + */ + fun pendingSend(): Pair? { + return when { + localSendState != null -> SendTarget.Event to localSendState + editSendState != null -> SendTarget.Edit to editSendState + redactionSendState != null -> SendTarget.Redaction to redactionSendState + else -> null + } + } } data class GroupedEvents( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt index 15ee42d575..02f720bed2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt @@ -50,16 +50,18 @@ fun SendFailureDialogView( ) } + val (sendTarget, _) = sendFailureDialogState.event.pendingSend() ?: null to null + ConfirmationDialog( modifier = modifier, title = stringResource(id = CommonStrings.common_sending_failed), content = content, onDismiss = onDismiss, submitText = stringResource(id = CommonStrings.action_retry), - onSubmitClick = { onRetry(sendFailureDialogState.event) }, + onSubmitClick = { sendTarget?.let { onRetry(sendFailureDialogState.event) } }, onCancelClick = onDismiss, thirdButtonText = stringResource(id = CommonStrings.action_remove_message), - onThirdButtonClick = { onRemoveMessage(sendFailureDialogState.event) }, + onThirdButtonClick = { sendTarget?.let { onRemoveMessage(sendFailureDialogState.event) } }, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 17d2a2260d..3d3187af96 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -93,7 +93,6 @@ import io.element.android.libraries.matrix.test.A_THREAD_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.core.FakeSendHandle import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser @@ -646,24 +645,24 @@ class MessagesPresenterTest { } @Test - fun `present - handle action redact - a message which was never sent is redacted without asking`() = runTest { + fun `present - handle action redact - a message which was never sent is removed from the send queue without asking`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) - val liveTimeline = FakeTimeline() - val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) } - liveTimeline.redactEventLambda = redactEventLambda + val abortSendLambda = lambdaRecorder { _: EventOrTransactionId -> Result.success(true) } val presenter = createMessagesPresenter( - timeline = liveTimeline, + timeline = FakeTimeline(abortSendResult = abortSendLambda), coroutineDispatchers = coroutineDispatchers, ) presenter.testWithLifecycleOwner { val initialState = awaitItem() - val localEcho = aMessageEvent(eventId = null, transactionId = A_TRANSACTION_ID) + val localEcho = aMessageEvent( + eventId = null, + transactionId = A_TRANSACTION_ID, + sendState = LocalEventSendState.Failed.Unknown("Error"), + ) initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, localEcho)) advanceUntilIdle() assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized) - assert(redactEventLambda) - .isCalledOnce() - .with(value(localEcho.eventOrTransactionId), value(null)) + abortSendLambda.assertions().isCalledOnce().with(value(localEcho.eventOrTransactionId)) } } @@ -1177,29 +1176,27 @@ class MessagesPresenterTest { @Test fun `present - handle action retry sending`() = runTest { - val retryLambda = lambdaRecorder> { Result.success(Unit) } + val retryLambda = lambdaRecorder { _: EventOrTransactionId -> Result.success(true) } val messageEvent = aMessageEvent( sendState = LocalEventSendState.Failed.Unknown("Error"), - sendHandleProvider = { FakeSendHandle(retryLambda = retryLambda) }, ) - val presenter = createMessagesPresenter() + val presenter = createMessagesPresenter(timeline = FakeTimeline(retrySendResult = retryLambda)) presenter.testWithLifecycleOwner { skipItems(1) val initialState = awaitItem() initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, messageEvent)) advanceUntilIdle() - retryLambda.assertions().isCalledOnce() + retryLambda.assertions().isCalledOnce().with(value(messageEvent.eventOrTransactionId)) } } @Test fun `present - handle action retry sending - failure is ignored`() = runTest { - val retryLambda = lambdaRecorder> { Result.failure(AN_EXCEPTION) } + val retryLambda = lambdaRecorder { _: EventOrTransactionId -> Result.failure(AN_EXCEPTION) } val messageEvent = aMessageEvent( sendState = LocalEventSendState.Failed.Unknown("Error"), - sendHandleProvider = { FakeSendHandle(retryLambda = retryLambda) }, ) - val presenter = createMessagesPresenter() + val presenter = createMessagesPresenter(timeline = FakeTimeline(retrySendResult = retryLambda)) presenter.testWithLifecycleOwner { skipItems(1) val initialState = awaitItem() @@ -1210,18 +1207,16 @@ class MessagesPresenterTest { } @Test - fun `present - handle action retry sending - no send handle, it should have no effect`() = runTest { - val messageEvent = aMessageEvent( - sendState = LocalEventSendState.Failed.Unknown("Error"), - sendHandleProvider = { null }, - ) - val presenter = createMessagesPresenter() + fun `present - handle action retry sending - no pending send state, it should have no effect`() = runTest { + val retryLambda = lambdaRecorder { _: EventOrTransactionId -> Result.success(true) } + val messageEvent = aMessageEvent(sendState = null) + val presenter = createMessagesPresenter(timeline = FakeTimeline(retrySendResult = retryLambda)) presenter.testWithLifecycleOwner { skipItems(1) val initialState = awaitItem() initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, messageEvent)) advanceUntilIdle() - // No op! + retryLambda.assertions().isNeverCalled() } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 677b2ac10b..e8613d3547 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -606,7 +606,7 @@ class MessagesViewTest : RobolectricTest() { timelineEventsRecorder.clear() clickOn(CommonStrings.action_remove_message) - messagesEventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Redact, timelineItem)) + messagesEventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.AbortSending, timelineItem)) timelineEventsRecorder.assertSingle(TimelineEvent.HideSendFailureDialog) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt index 1b9394f2ee..87dbd2119b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt @@ -43,7 +43,7 @@ internal fun aMessageEvent( content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = A_MESSAGE, isEdited = false), inReplyTo: InReplyToDetails? = null, threadInfo: TimelineItemThreadInfo? = null, - sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID), + sendState: LocalEventSendState? = LocalEventSendState.Sent(AN_EVENT_ID), debugInfoProvider: TimelineItemDebugInfoProvider = TimelineItemDebugInfoProvider { aTimelineItemDebugInfo() }, messageShieldProvider: MessageShieldProvider = MessageShieldProvider { null }, sendHandleProvider: SendHandleProvider = SendHandleProvider { FakeSendHandle() } @@ -62,6 +62,8 @@ internal fun aMessageEvent( reactionsState = aTimelineItemReactions(count = 0), readReceiptState = TimelineItemReadReceipts(emptyList().toImmutableList()), localSendState = sendState, + editSendState = null, + redactionSendState = null, inReplyTo = inReplyTo, threadInfo = threadInfo, origin = null, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index f6ef4f0fc2..b9bb13649a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -43,6 +43,8 @@ class TimelineItemGrouperTest { reactionsState = aTimelineItemReactions(count = 0), readReceiptState = TimelineItemReadReceipts(emptyList().toImmutableList()), localSendState = LocalEventSendState.Sent(AN_EVENT_ID), + editSendState = null, + redactionSendState = null, isEditable = false, canBeRepliedTo = false, inReplyTo = null, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt index 9a5eda366b..ae832ade45 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt @@ -83,6 +83,8 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf( isOwn = false, isRemote = false, localSendState = null, + editSendState = null, + redactionSendState = null, reactions = persistentListOf(), receipts = persistentListOf(), sender = A_USER_ID, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 51a8964aa0..d26d8cc00c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.timeline.item.SendTarget import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId @@ -423,4 +424,20 @@ interface Timeline : AutoCloseable { * Get the latest event id of the timeline, or `null` when it holds no event yet. */ suspend fun getLatestEventId(): Result + + /** + * Retries sending an event that failed to send, by putting it back in the send queue. + * + * @param eventOrTransactionId the event or transaction ID of the event to retry sending. + * @param sendTarget the target action of the send operation. + */ + suspend fun retrySend(eventOrTransactionId: EventOrTransactionId, sendTarget: SendTarget): Result + + /** + * Aborts sending an event that failed to send, by removing it from the send queue. + * + * @param eventOrTransactionId the event or transaction ID of the event to abort sending. + * @param sendTarget the target action of the send operation. + */ + suspend fun abortSend(eventOrTransactionId: EventOrTransactionId, sendTarget: SendTarget): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/SendTarget.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/SendTarget.kt new file mode 100644 index 0000000000..b882636e8e --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/SendTarget.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.timeline.item + +import androidx.compose.runtime.Immutable + +/** + * Represents the target action of a send operation in a timeline. + */ +@Immutable +sealed interface SendTarget { + /** Send a new event. */ + data object Event : SendTarget + + /** Edit an existing event. */ + data object Edit : SendTarget + + /** Redact an existing event. */ + data object Redaction : SendTarget + + /** React to an existing event with the given key. */ + data class Reaction(val key: String) : SendTarget +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index 8a3221677d..c2c110dd5f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -24,6 +24,8 @@ data class EventTimelineItem( val isOwn: Boolean, val isRemote: Boolean, val localSendState: LocalEventSendState?, + val editSendState: LocalEventSendState?, + val redactionSendState: LocalEventSendState?, val reactions: ImmutableList, val receipts: ImmutableList, val sender: UserId, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 70dbff1209..a9eb22cc8b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -877,6 +877,7 @@ class RustMatrixClient( override fun sendQueueDisabledFlow(): Flow = mxCallbackFlow { innerClient.subscribeToSendQueueStatus(object : SendQueueRoomErrorListener { override fun onError(roomId: String, error: ClientException) { + Timber.e(error, "Send queue error for room $roomId") trySend(RoomId(roomId)) } }) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index c7dd260d47..d875ac0429 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineException +import io.element.android.libraries.matrix.api.timeline.item.SendTarget import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.impl.media.GalleryMediaUploadHandlerImpl @@ -697,6 +698,18 @@ class RustTimeline( } } + override suspend fun retrySend(eventOrTransactionId: EventOrTransactionId, sendTarget: SendTarget): Result = withContext(dispatcher) { + runCatchingExceptions { + inner.retrySend(eventOrTransactionId.toRustEventOrTransactionId(), sendTarget.map()) + } + } + + override suspend fun abortSend(eventOrTransactionId: EventOrTransactionId, sendTarget: SendTarget): Result = withContext(dispatcher) { + runCatchingExceptions { + inner.abortSend(eventOrTransactionId.toRustEventOrTransactionId(), sendTarget.map()) + } + } + private suspend fun fetchDetailsForEvent(eventId: EventId): Result = withContext(dispatcher) { runCatchingExceptions { inner.fetchDetailsForEvent(eventId.value) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/SendTarget.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/SendTarget.kt new file mode 100644 index 0000000000..889735cad9 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/SendTarget.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline + +import io.element.android.libraries.matrix.api.timeline.item.SendTarget + +fun SendTarget.map() = when (this) { + is SendTarget.Event -> org.matrix.rustcomponents.sdk.SendTarget.Event + is SendTarget.Edit -> org.matrix.rustcomponents.sdk.SendTarget.Edit + is SendTarget.Redaction -> org.matrix.rustcomponents.sdk.SendTarget.Redaction + is SendTarget.Reaction -> org.matrix.rustcomponents.sdk.SendTarget.Reaction(key) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index f77e690ebf..3225dffc1c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -51,6 +51,8 @@ class EventTimelineItemMapper( isOwn = isOwn, isRemote = isRemote, localSendState = localSendState?.map(), + editSendState = editSendState?.map(), + redactionSendState = redactionSendState?.map(), reactions = reactions.map(), receipts = readReceipts.map(), sender = UserId(sender), @@ -113,11 +115,7 @@ fun RustEventSendState?.map(): LocalEventSendState? { ) } is QueueWedgeError.GenericApiError -> { - if (isRecoverable) { - LocalEventSendState.Sending.Event - } else { - LocalEventSendState.Failed.Unknown(queueWedgeError.msg) - } + LocalEventSendState.Failed.Unknown(queueWedgeError.msg) } is QueueWedgeError.InvalidMimeType -> { LocalEventSendState.Failed.InvalidMimeType(queueWedgeError.mimeType) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index 88dcb13a8c..07c4825cb9 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.SendTarget import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler @@ -60,7 +61,9 @@ class FakeTimeline( receiptType: ReceiptType, ) -> Result = { _, _ -> lambdaError() - } + }, + private val retrySendResult: (eventOrTransactionId: EventOrTransactionId) -> Result = { lambdaError() }, + private val abortSendResult: (eventOrTransactionId: EventOrTransactionId) -> Result = { lambdaError() }, ) : Timeline { var sendMessageLambda: ( body: String, @@ -477,6 +480,14 @@ class FakeTimeline( return getLatestEventIdResult() } + override suspend fun retrySend(eventOrTransactionId: EventOrTransactionId, sendTarget: SendTarget): Result { + return retrySendResult(eventOrTransactionId) + } + + override suspend fun abortSend(eventOrTransactionId: EventOrTransactionId, sendTarget: SendTarget): Result { + return abortSendResult(eventOrTransactionId) + } + var closeCounter = 0 private set diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt index da8b872ef0..c1e156caf6 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt @@ -52,6 +52,8 @@ fun anEventTimelineItem( isOwn: Boolean = false, isRemote: Boolean = false, localSendState: LocalEventSendState? = null, + editSendState: LocalEventSendState? = null, + redactionSendState: LocalEventSendState? = null, reactions: ImmutableList = persistentListOf(), receipts: ImmutableList = persistentListOf(), sender: UserId = A_USER_ID, @@ -69,6 +71,8 @@ fun anEventTimelineItem( isOwn = isOwn, isRemote = isRemote, localSendState = localSendState, + editSendState = editSendState, + redactionSendState = redactionSendState, reactions = reactions, receipts = receipts, sender = sender, diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en.png index c3e5011041..4dcdb1b9ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cd23b8ab206543d9632589c9c6e3ae157846cbbeef683d187f0fb8a7475b27d -size 6698 +oid sha256:b161e07f504adbb140c80945cf9cd5b31dad4edeeb86d1b683fef4f02148a110 +size 12178 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en.png index 4dcdb1b9ae..63b599fe61 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b161e07f504adbb140c80945cf9cd5b31dad4edeeb86d1b683fef4f02148a110 -size 12178 +oid sha256:df393203d4535dd63962cae667253c00c19dc4b6e25c887619ff5e833066f4ba +size 17596 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en.png index 63b599fe61..584992b643 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df393203d4535dd63962cae667253c00c19dc4b6e25c887619ff5e833066f4ba -size 17596 +oid sha256:6f4886f895d90cbd175fab866329841b45a13890b923ee7928673698d320b60f +size 22940 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en.png index 584992b643..659db847cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f4886f895d90cbd175fab866329841b45a13890b923ee7928673698d320b60f -size 22940 +oid sha256:541e50bba5a4dd70015bbc5410d8d8c9b0cd194ab119918f64baf73cefdd618d +size 27956 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en.png index 659db847cc..3fe4168506 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:541e50bba5a4dd70015bbc5410d8d8c9b0cd194ab119918f64baf73cefdd618d -size 27956 +oid sha256:2023632334cfbd15b399fce33cb5ac04135f371d6cb09669fd66f97bc86e8fae +size 32655 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en.png index 15ae66d08f..584bac4e93 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7580ba96fcc879011f591ac4bacd16d3c7cdc7db51f39cc7989c7c4dffdaa69 -size 5937 +oid sha256:eaf905401acff2b0000abc9c7a13b8034c55d12e355908dded5b1a24fe573fb8 +size 11366 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en.png index 584bac4e93..43aaf422f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eaf905401acff2b0000abc9c7a13b8034c55d12e355908dded5b1a24fe573fb8 -size 11366 +oid sha256:e6dc3ec6b43eda48a6d96af60aa983c2a8c42d4da81ef48dc2bd783309de8316 +size 16469 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en.png index 43aaf422f2..6382583d2d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6dc3ec6b43eda48a6d96af60aa983c2a8c42d4da81ef48dc2bd783309de8316 -size 16469 +oid sha256:fa666b0e8ec6d77820c31ebeed2ba9fc9868d07e56eb77ff079b5cd2b2840224 +size 21836 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en.png index 6382583d2d..e9321c5510 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa666b0e8ec6d77820c31ebeed2ba9fc9868d07e56eb77ff079b5cd2b2840224 -size 21836 +oid sha256:e260c421c187424b8d139e75351aa3e654fcf28975aebc4bbcefd8f77c2d21d6 +size 27082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en.png index e9321c5510..db2b180cfd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e260c421c187424b8d139e75351aa3e654fcf28975aebc4bbcefd8f77c2d21d6 -size 27082 +oid sha256:48537a0e326d7cf1fc36060fd2831bfa3b237806a79b492368471de25f331a58 +size 32174 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_10_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_8_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_9_en.png new file mode 100644 index 0000000000..c7b23e6596 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d4c593002d2ed5f6ace886d9220f3d236356cf1cefd8f7c0da68177032d0bc +size 4561 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_10_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_8_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_9_en.png new file mode 100644 index 0000000000..ac2beb8344 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bc4fe868c3386c0ff38ee807ccfd098943bdb08595372378cffc0df44956b2c +size 4448