From 7ddf93ed09b75e2114e1f9f80118cc1da93e6b87 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 15 Jun 2023 11:27:37 +0200 Subject: [PATCH] [Message Actions] Retry sending failed messages (#596) * Add `RetrySendMessageMenu` to retry sending failed messages or removing its local echo. * Fix initial event being retrieved, not the updated one --------- Co-authored-by: ElementBot --- changelog.d/487.feature | 1 + .../messages/impl/MessagesPresenter.kt | 4 + .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 5 + .../features/messages/impl/MessagesView.kt | 25 ++- .../impl/timeline/TimelineStateProvider.kt | 2 + .../messages/impl/timeline/TimelineView.kt | 17 +- .../components/TimelineEventTimestampView.kt | 13 +- .../components/TimelineItemEventRow.kt | 13 +- .../retrysendmenu/RetrySendMenuEvents.kt | 26 +++ .../retrysendmenu/RetrySendMenuPresenter.kt | 72 ++++++++ .../retrysendmenu/RetrySendMenuState.kt | 26 +++ .../RetrySendMenuStateProvider.kt | 31 ++++ .../retrysendmenu/RetrySendMessageMenu.kt | 168 ++++++++++++++++++ .../event/TimelineItemEventFactory.kt | 1 + .../impl/timeline/model/TimelineItem.kt | 1 + .../impl/src/main/res/values/localazy.xml | 3 + .../messages/MessagesPresenterTest.kt | 4 + .../RetrySendMenuPresenterTests.kt | 161 +++++++++++++++++ .../libraries/matrix/api/room/MatrixRoom.kt | 4 + .../matrix/api/timeline/MatrixTimelineItem.kt | 1 + .../timeline/item/event/EventTimelineItem.kt | 1 + .../matrix/impl/room/RustMatrixRoom.kt | 15 ++ .../item/event/EventTimelineItemMapper.kt | 1 + .../android/libraries/matrix/test/TestData.kt | 1 + .../matrix/test/room/FakeMatrixRoom.kt | 26 +++ .../matrix/test/room/RoomSummaryFixture.kt | 2 + ...nuPreviewDark_0_null_0,NEXUS_5,1.0,en].png | 3 + ...nuPreviewDark_0_null_1,NEXUS_5,1.0,en].png | 3 + ...uPreviewLight_0_null_0,NEXUS_5,1.0,en].png | 3 + ...uPreviewLight_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...wDarkPreview_0_null_10,NEXUS_5,1.0,en].png | 4 +- ...LightPreview_0_null_10,NEXUS_5,1.0,en].png | 4 +- 41 files changed, 641 insertions(+), 37 deletions(-) create mode 100644 changelog.d/487.feature create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_1,NEXUS_5,1.0,en].png diff --git a/changelog.d/487.feature b/changelog.d/487.feature new file mode 100644 index 0000000000..ee1106eb58 --- /dev/null +++ b/changelog.d/487.feature @@ -0,0 +1 @@ +Add menu to retry sending failed messages or delete their local echoes. 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 de2916a6b4..96384a295c 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 @@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent @@ -65,6 +66,7 @@ class MessagesPresenter @Inject constructor( private val composerPresenter: MessageComposerPresenter, private val timelinePresenter: TimelinePresenter, private val actionListPresenter: ActionListPresenter, + private val retrySendMenuPresenter: RetrySendMenuPresenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val messageSummaryFormatter: MessageSummaryFormatter, @@ -77,6 +79,7 @@ class MessagesPresenter @Inject constructor( val composerState = composerPresenter.present() val timelineState = timelinePresenter.present() val actionListState = actionListPresenter.present() + val retryState = retrySendMenuPresenter.present() val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L) val roomName: MutableState = rememberSaveable { @@ -116,6 +119,7 @@ class MessagesPresenter @Inject constructor( composerState = composerState, timelineState = timelineState, actionListState = actionListState, + retrySendMenuState = retryState, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, snackbarMessage = snackbarMessage, eventSink = ::handleEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 8c876ea49c..7b53421b39 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.timeline.TimelineState +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId @@ -32,6 +33,7 @@ data class MessagesState( val composerState: MessageComposerState, val timelineState: TimelineState, val actionListState: ActionListState, + val retrySendMenuState: RetrySendMenuState, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val eventSink: (MessagesEvents) -> Unit diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index e37fd11540..c3cd05d823 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -48,6 +49,10 @@ fun aMessagesState() = MessagesState( timelineState = aTimelineState().copy( timelineItems = aTimelineItemList(aTimelineItemTextContent()), ), + retrySendMenuState = RetrySendMenuState( + selectedEvent = null, + eventSink = {}, + ), actionListState = anActionListState(), hasNetworkConnection = true, snackbarMessage = null, 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 542f48e1ae..fb618fbba2 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 @@ -34,14 +34,10 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -59,7 +55,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.messages.impl.actionlist.ActionListEvents -import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment @@ -67,6 +62,8 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.components.CustomReactionBottomSheet +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.androidutils.ui.hideKeyboard @@ -85,6 +82,7 @@ import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState 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.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import timber.log.Timber @@ -176,6 +174,11 @@ fun MessagesView( onMessageClicked = ::onMessageClicked, onMessageLongClicked = ::onMessageLongClicked, onUserDataClicked = onUserDataClicked, + onTimestampClicked = { event -> + if (event.sendState is EventSendState.SendingFailed) { + state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event)) + } + } ) }, snackbarHost = { @@ -225,6 +228,10 @@ fun MessagesView( } } ) + + RetrySendMessageMenu( + state = state.retrySendMenuState + ) } @Composable @@ -244,10 +251,11 @@ private fun AttachmentStateView( @Composable fun MessagesViewContent( state: MessagesState, + onMessageClicked: (TimelineItem.Event) -> Unit, + onUserDataClicked: (UserId) -> Unit, + onMessageLongClicked: (TimelineItem.Event) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, - onMessageClicked: (TimelineItem.Event) -> Unit = {}, - onUserDataClicked: (UserId) -> Unit = {}, - onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, ) { Column( modifier = modifier @@ -263,6 +271,7 @@ fun MessagesViewContent( onMessageClicked = onMessageClicked, onMessageLongClicked = onMessageLongClicked, onUserDataClicked = onUserDataClicked, + onTimestampClicked = onTimestampClicked, ) } MessageComposerView( 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 47e09fc577..c170886672 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 @@ -94,6 +94,7 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList internal fun aTimelineItemEvent( eventId: EventId = EventId("\$" + Random.nextInt().toString()), + transactionId: String? = null, isMine: Boolean = false, content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, @@ -104,6 +105,7 @@ internal fun aTimelineItemEvent( return TimelineItem.Event( id = eventId.value, eventId = eventId, + transactionId = transactionId, senderId = UserId("@senderId:domain"), senderAvatar = AvatarData("@senderId:domain", "sender"), content = content, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 4c6416ec67..c73cce50f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -68,10 +68,11 @@ import kotlinx.coroutines.launch @Composable fun TimelineView( state: TimelineState, + onUserDataClicked: (UserId) -> Unit, + onMessageClicked: (TimelineItem.Event) -> Unit, + onMessageLongClicked: (TimelineItem.Event) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, - onUserDataClicked: (UserId) -> Unit = {}, - onMessageClicked: (TimelineItem.Event) -> Unit = {}, - onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, ) { fun onReachedLoadMore() { state.eventSink(TimelineEvents.LoadMore) @@ -102,6 +103,7 @@ fun TimelineView( onLongClick = onMessageLongClicked, onUserDataClick = onUserDataClicked, inReplyToClick = ::inReplyToClicked, + onTimestampClicked = onTimestampClicked, ) if (index == state.timelineItems.lastIndex) { onReachedLoadMore() @@ -125,6 +127,7 @@ fun TimelineItemRow( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier ) { when (timelineItem) { @@ -159,6 +162,7 @@ fun TimelineItemRow( onLongClick = ::onLongClick, onUserDataClick = onUserDataClick, inReplyToClick = inReplyToClick, + onTimestampClicked = onTimestampClicked, modifier = modifier, ) } @@ -191,6 +195,7 @@ fun TimelineItemRow( onLongClick = onLongClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, + onTimestampClicked = onTimestampClicked, ) } } @@ -276,6 +281,10 @@ fun TimelineViewDarkPreview( private fun ContentToPreview(content: TimelineItemEventContent) { val timelineItems = aTimelineItemList(content) TimelineView( - state = aTimelineState(timelineItems) + state = aTimelineState(timelineItems), + onMessageClicked = {}, + onTimestampClicked = {}, + onUserDataClicked = {}, + onMessageLongClicked = {}, ) } 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 ed4febf982..530f62a188 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 @@ -17,12 +17,15 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -54,7 +57,15 @@ fun TimelineEventTimestampView( val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse() val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null Row( - modifier = modifier.clickable(onClick = onClick), + modifier = Modifier + .clickable( + onClick = onClick, + enabled = true, + indication = rememberRipple(bounded = false), + interactionSource = MutableInteractionSource() + ) + .padding(start = 16.dp) // Add extra padding for touch target size + .then(modifier), verticalAlignment = Alignment.CenterVertically, ) { if (isMessageEdited) { 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 f84a899c6b..4f07f66f5c 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 @@ -75,6 +75,7 @@ fun TimelineItemEventRow( onLongClick: () -> Unit, onUserDataClick: (UserId) -> Unit, inReplyToClick: (EventId) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } @@ -83,7 +84,7 @@ fun TimelineItemEventRow( onUserDataClick(event.senderId) } - fun inReplayToClicked() { + fun inReplyToClicked() { val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return inReplyToClick(inReplyToEventId) } @@ -131,7 +132,10 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onMessageClick = onClick, onMessageLongClick = onLongClick, - inReplyToClick = ::inReplayToClicked, + inReplyToClick = ::inReplyToClicked, + onTimestampClicked = { + onTimestampClicked(event) + } ) } TimelineItemReactionsView( @@ -177,6 +181,7 @@ private fun MessageEventBubbleContent( onMessageClick: () -> Unit, onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, + onTimestampClicked: () -> Unit, modifier: Modifier = Modifier ) { val isMediaItem = event.content is TimelineItemImageContent || event.content is TimelineItemVideoContent @@ -207,7 +212,7 @@ private fun MessageEventBubbleContent( ContentView(modifier = contentModifier) TimelineEventTimestampView( event = event, - onClick = onMessageClick, + onClick = onTimestampClicked, modifier = timestampModifier .padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding .background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp)) @@ -220,7 +225,7 @@ private fun MessageEventBubbleContent( ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp)) TimelineEventTimestampView( event = event, - onClick = onMessageClick, + onClick = onTimestampClicked, modifier = timestampModifier .align(Alignment.End) .padding(horizontal = 8.dp, vertical = 2.dp) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt new file mode 100644 index 0000000000..ab6e32f078 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.retrysendmenu + +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +sealed interface RetrySendMenuEvents { + data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents + object RetrySend : RetrySendMenuEvents + object RemoveFailed : RetrySendMenuEvents + object Dismiss: RetrySendMenuEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt new file mode 100644 index 0000000000..237dc5683d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.retrysendmenu + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.coroutines.launch +import javax.inject.Inject + +class RetrySendMenuPresenter @Inject constructor( + private val room: MatrixRoom, +) : Presenter { + + @Composable + override fun present(): RetrySendMenuState { + val coroutineScope = rememberCoroutineScope() + var selectedEvent: TimelineItem.Event? by remember { mutableStateOf(null) } + + fun handleEvent(event: RetrySendMenuEvents) { + when (event) { + is RetrySendMenuEvents.EventSelected -> { + selectedEvent = event.event + } + RetrySendMenuEvents.RetrySend -> { + coroutineScope.launch { + selectedEvent?.transactionId?.let { transactionId -> + room.retrySendMessage(transactionId) + } + selectedEvent = null + } + } + RetrySendMenuEvents.RemoveFailed -> { + coroutineScope.launch { + selectedEvent?.transactionId?.let { transactionId -> + room.cancelSend(transactionId) + } + selectedEvent = null + } + } + RetrySendMenuEvents.Dismiss -> { + selectedEvent = null + } + } + } + + return RetrySendMenuState( + selectedEvent = selectedEvent, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt new file mode 100644 index 0000000000..e10e9c752c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.retrysendmenu + +import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +@Immutable +data class RetrySendMenuState( + val selectedEvent: TimelineItem.Event?, + val eventSink: (RetrySendMenuEvents) -> Unit, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt new file mode 100644 index 0000000000..ccb5c26982 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.retrysendmenu + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +class RetrySendMenuStateProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + aRetrySendMenuState(event = null), + aRetrySendMenuState(event = aTimelineItemEvent()), + ) +} + +fun aRetrySendMenuState(event: TimelineItem.Event? = aTimelineItemEvent()) = + RetrySendMenuState(selectedEvent = event, eventSink = {}) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt new file mode 100644 index 0000000000..9d8c8283ca --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.retrysendmenu + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.LocalColors +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.features.messages.impl.R +import kotlinx.coroutines.launch + +@Composable +internal fun RetrySendMessageMenu( + state: RetrySendMenuState, + modifier: Modifier = Modifier, +) { + val isVisible = state.selectedEvent != null + + fun onDismiss() { + state.eventSink(RetrySendMenuEvents.Dismiss) + } + + fun onRetry() { + state.eventSink(RetrySendMenuEvents.RetrySend) + } + + fun onRemoveFailed() { + state.eventSink(RetrySendMenuEvents.RemoveFailed) + } + + RetrySendMessageMenuBottomSheet( + modifier = modifier, + isVisible = isVisible, + onRetry = ::onRetry, + onRemoveFailed = ::onRemoveFailed, + onDismiss = ::onDismiss + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun RetrySendMessageMenuBottomSheet( + isVisible: Boolean, + onRetry: () -> Unit, + onRemoveFailed: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val sheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + if (isVisible) { + ModalBottomSheet( + modifier = modifier, +// modifier = modifier.navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044 +// .imePadding() + sheetState = sheetState, + onDismissRequest = { + coroutineScope.launch { + sheetState.hide() + onDismiss() + } + } + ) { + RetrySendMenuContents(onRetry = onRetry, onRemoveFailed = onRemoveFailed) + // FIXME remove after https://issuetracker.google.com/issues/275849044 + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ColumnScope.RetrySendMenuContents( + onRetry: () -> Unit, + onRemoveFailed: () -> Unit, + sheetState: SheetState = rememberModalBottomSheetState(), +) { + val coroutineScope = rememberCoroutineScope() + + ListItem(headlineContent = { + Text(stringResource(R.string.screen_room_retry_send_menu_title), fontWeight = FontWeight.Medium) + }) + ListItem( + headlineContent = { + Text(stringResource(R.string.screen_room_retry_send_menu_send_again_action)) + }, + modifier = Modifier.clickable { + coroutineScope.launch { + sheetState.hide() + onRetry() + } + } + ) + ListItem( + headlineContent = { + Text(stringResource(R.string.screen_room_retry_send_menu_remove_action)) + }, + colors = ListItemDefaults.colors(headlineColor = LocalColors.current.textActionCritical), + modifier = Modifier.clickable { + coroutineScope.launch { + sheetState.hide() + onRemoveFailed() + } + } + ) +} + +@Preview +@Composable +internal fun RetrySendMessageMenuPreviewLight(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) { + ElementPreviewLight { + ContentToPreview(state) + } +} + +@Preview +@Composable +internal fun RetrySendMessageMenuPreviewDark(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) { + ElementPreviewDark { + ContentToPreview(state) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ContentToPreview(state: RetrySendMenuState) { + // TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed + Column { + RetrySendMenuContents( + onRetry = {}, + onRemoveFailed = {}, + ) + } +} 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 ce9a558b97..62aca35d6e 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 @@ -72,6 +72,7 @@ class TimelineItemEventFactory @Inject constructor( return TimelineItem.Event( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, + transactionId = currentTimelineItem.transactionId, senderId = currentSender, senderDisplayName = senderDisplayName, senderAvatar = senderAvatarData, 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 0328bf6603..08f9df0535 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 @@ -52,6 +52,7 @@ sealed interface TimelineItem { data class Event( val id: String, val eventId: EventId? = null, + val transactionId: String? = null, val senderId: UserId, val senderDisplayName: String?, val senderAvatar: AvatarData, diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index d94f32a88f..16672f15ec 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -10,5 +10,8 @@ "Attachment" "Photo & Video Library" "Could not retrieve user details" + "Send again" + "Your message failed to send" "Failed processing media to upload, please try again." + "Remove" \ No newline at end of file diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 1ced503920..4238a56423 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent @@ -322,15 +323,18 @@ class MessagesPresenterTest { flavorShortDescription = "", ) val actionListPresenter = ActionListPresenter(buildMeta = buildMeta) + val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, timelinePresenter = timelinePresenter, actionListPresenter = actionListPresenter, + retrySendMenuPresenter = retrySendMenuPresenter, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), messageSummaryFormatter = FakeMessageSummaryFormatter(), dispatchers = testCoroutineDispatchers(), ) + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt new file mode 100644 index 0000000000..1e467b82af --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.timeline.components.retrysendmenu + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter +import io.element.android.libraries.matrix.test.A_TRANSACTION_ID +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RetrySendMenuPresenterTests { + + private val room = FakeMatrixRoom() + private val presenter = RetrySendMenuPresenter(room) + + @Test + fun `present - handle event selected`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent() + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + + assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent) + } + } + + @Test + fun `present - handle dismiss`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent() + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.Dismiss) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle resend with transactionId`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RetrySend) + assertThat(room.retrySendMessageCount).isEqualTo(1) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle resend without transactionId`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = null) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RetrySend) + assertThat(room.retrySendMessageCount).isEqualTo(0) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle resend with error`() = runTest { + room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error"))) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RetrySend) + assertThat(room.retrySendMessageCount).isEqualTo(1) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle remove failed message with transactionId`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + assertThat(room.cancelSendCount).isEqualTo(1) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle remove failed message without transactionId`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = null) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + assertThat(room.cancelSendCount).isEqualTo(0) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle remove failed message with error`() = runTest { + room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error"))) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + assertThat(room.cancelSendCount).isEqualTo(1) + assertThat(awaitItem().selectedEvent).isNull() + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 92374b5b00..0ddd75966b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -83,6 +83,10 @@ interface MatrixRoom : Closeable { suspend fun sendReaction(emoji: String, eventId: EventId): Result + suspend fun retrySendMessage(transactionId: String): Result + + suspend fun cancelSend(transactionId: String): Result + suspend fun leave(): Result suspend fun acceptInvitation(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt index 547e593a42..f84f1875e4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt @@ -24,6 +24,7 @@ sealed interface MatrixTimelineItem { data class Event(val event: EventTimelineItem) : MatrixTimelineItem { val uniqueId: String = event.uniqueIdentifier val eventId: EventId? = event.eventId + val transactionId: String? = event.transactionId } data class Virtual(val virtual: VirtualTimelineItem) : MatrixTimelineItem 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 05f440c413..2a5c068519 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 @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn data class EventTimelineItem( val uniqueIdentifier: String, val eventId: EventId?, + val transactionId: String?, val isEditable: Boolean, val isLocal: Boolean, val isOwn: Boolean, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 177076ed28..8452ef193b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -264,6 +265,20 @@ class RustMatrixRoom( } } + override suspend fun retrySendMessage(transactionId: String): Result = + withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.retrySend(transactionId) + } + } + + override suspend fun cancelSend(transactionId: String): Result = + withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.cancelSend(transactionId) + } + } + @OptIn(ExperimentalUnsignedTypes::class) override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = withContext(coroutineDispatchers.io) { 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 a77ddbd80f..bbb9c8fe2a 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 @@ -35,6 +35,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap EventTimelineItem( uniqueIdentifier = it.uniqueIdentifier(), eventId = it.eventId()?.let(::EventId), + transactionId = it.transactionId(), isEditable = it.isEditable(), isLocal = it.isLocal(), isOwn = it.isOwn(), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 7e54d8e851..38234d2466 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -37,6 +37,7 @@ val A_ROOM_ID_2 = RoomId("!aRoomId2:domain") val A_THREAD_ID = ThreadId("\$aThreadId") val AN_EVENT_ID = EventId("\$anEventId") val AN_EVENT_ID_2 = EventId("\$anEventId2") +const val A_TRANSACTION_ID = "aTransactionId" const val A_UNIQUE_ID = "aUniqueId" const val A_ROOM_NAME = "A room name" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 1199d94ac4..9aa244f503 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -71,6 +71,8 @@ class FakeMatrixRoom( private var updateAvatarResult = Result.success(Unit) private var removeAvatarResult = Result.success(Unit) private var sendReactionResult = Result.success(Unit) + private var retrySendMessageResult = Result.success(Unit) + private var cancelSendResult = Result.success(Unit) var sendMediaCount = 0 private set @@ -78,6 +80,12 @@ class FakeMatrixRoom( var sendReactionCount = 0 private set + var retrySendMessageCount: Int = 0 + private set + + var cancelSendCount: Int = 0 + private set + var isInviteAccepted: Boolean = false private set @@ -133,6 +141,16 @@ class FakeMatrixRoom( return sendReactionResult } + override suspend fun retrySendMessage(transactionId: String): Result { + retrySendMessageCount++ + return retrySendMessageResult + } + + override suspend fun cancelSend(transactionId: String): Result { + cancelSendCount++ + return cancelSendResult + } + var editMessageParameter: String? = null private set @@ -292,4 +310,12 @@ class FakeMatrixRoom( fun givenSendReactionResult(result: Result) { sendReactionResult = result } + + fun givenRetrySendMessageResult(result: Result) { + retrySendMessageResult = result + } + + fun givenCancelSendResult(result: Result) { + cancelSendResult = result + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index e6ac93a3ab..e8e3ff38b9 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -89,6 +89,7 @@ fun aRoomMessage( fun anEventTimelineItem( uniqueIdentifier: String = A_UNIQUE_ID, eventId: EventId = AN_EVENT_ID, + transactionId: String? = null, isEditable: Boolean = false, isLocal: Boolean = false, isOwn: Boolean = false, @@ -103,6 +104,7 @@ fun anEventTimelineItem( ) = EventTimelineItem( uniqueIdentifier = uniqueIdentifier, eventId = eventId, + transactionId = transactionId, isEditable = isEditable, isLocal = isLocal, isOwn = isOwn, diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..309f151dcc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:307bca06295570124970ed808f4a068f50086212623fa0e69630f52cf9e8752a +size 15440 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..309f151dcc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:307bca06295570124970ed808f4a068f50086212623fa0e69630f52cf9e8752a +size 15440 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af6c929a30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e47e880d5bf8ffedd0662ec0c791483a2cb76e921d12958afffa959b1370c270 +size 14073 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af6c929a30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e47e880d5bf8ffedd0662ec0c791483a2cb76e921d12958afffa959b1370c270 +size 14073 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index b244bdb73f..6434e7e2f4 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdf7fe891aa40f4d626733deb130481d64b3315531712128d1b4073e5ccedf19 -size 5394 +oid sha256:0f56dcf7dfb9ca58618e891aae28099a82457b889da3cd8ec613aeb3757e9750 +size 5403 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 25c4fffbab..573a379b18 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0baf33f0ec99cd8e4b201d72b38208a02b0511da544276a38d17ff5653e2b754 -size 5906 +oid sha256:b0c0973dd677105248cd17351ac32e9cf79dc6082eb3a9e26a7ff37e2475ca2f +size 5860 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 5aa7211ab3..f4a4fb3c17 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8d9dc01ec07f9e43032e3ec7610eda5588145f3003cc3d329ddf4325e19f239 -size 6674 +oid sha256:2fd42d7ae900d5ece9499b1877766d82d8d174d4b8b04633c93e1de04d3aa069 +size 6715 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 2ed9e07432..718fa7e4ad 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e0f86449d651c50966fe594a46e3a59ae9a5505013e3e36f2458be35ba1844e -size 7174 +oid sha256:e324ac2b0578de021d27cddbba851d5835086f80066cb53c1c1829241ac63fec +size 7172 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index f640d9af42..8af55ef3ae 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:621facc4799c0069fe83b4be9a5085ecafb042de1cdfbbf4ccf0c15548373aa9 -size 5332 +oid sha256:fc5e3c5e68dcab10bd67878cd28d1cf769b19cb4daa3dfc02a310b9e44e38d59 +size 5362 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 8ed48c31f6..c7059cab27 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:986f40750d69639e771c9bf1aac30c49dbf122aa60dc0280164a8868671b638a -size 5910 +oid sha256:8ec5e0cbd36f80526f2a55637d35c41eec7cd8dd07562bc9ed115b2886963b39 +size 5888 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 9735300d61..159f0b046a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d4fb8d68603309963d434f3166821055d818836b1ebfb4fc4c637aba992277d -size 6617 +oid sha256:961c557c4d7f77a6d1deb9bbb908cdd855f5e3ebee91e211c91afe857ba3785d +size 6593 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index d265165330..e0496389bb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fa641ca8d5c796c858b54ad997052aabf817bc86e254d38d4b33038bc21eedf -size 7285 +oid sha256:833ccceceff1f0a74db95058f3714da514525d05cd3e040619fb221e32ba8d33 +size 7287 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png index df3b482afd..9c924da5c9 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:052a95ccf790c0ef93bbfd24ff063fd5489b2c250a9c53ab6d58438653fbbea5 -size 46098 +oid sha256:259022501a602e5e6e22a800f866d221500f34f7ce475df9996b897f93e7fc49 +size 45929 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png index 9535f67822..09f6f440f9 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:744f2192c85b1d6a56cc2eb7f6c873eb45827d2ad2d36fd1678c4ace4118af2c -size 45945 +oid sha256:4fe9d2349725bb5c6dbfcfd29dcf8c7c10079deefdc8a0cb367dfe23cb453b85 +size 45873