diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 83cfd5f1f3..ecff658963 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -282,7 +282,7 @@ class LoggedInFlowNode @AssistedInject constructor( is PermalinkData.RoomLink -> { backstack.push( NavTarget.Room( - data.roomIdOrAlias, + roomIdOrAlias = data.roomIdOrAlias, initialElement = RoomNavigationTarget.Messages(data.eventId), // TODO Use the viaParameters ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index f3cc3dfd28..baf13dc0d3 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -83,7 +83,7 @@ class RoomFlowNode @AssistedInject constructor( data class Inputs( val roomIdOrAlias: RoomIdOrAlias, val roomDescription: Optional, - val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(), + val initialElement: RoomNavigationTarget, ) : NodeInputs private val inputs: Inputs = inputs() @@ -177,7 +177,10 @@ class RoomFlowNode @AssistedInject constructor( } is NavTarget.JoinedRoom -> { val roomFlowNodeCallback = plugins() - val inputs = JoinedRoomFlowNode.Inputs(navTarget.roomId, initialElement = inputs.initialElement) + val inputs = JoinedRoomFlowNode.Inputs( + roomId = navTarget.roomId, + initialElement = inputs.initialElement + ) createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt index ae8d03be33..49bcb53048 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt @@ -69,7 +69,7 @@ class JoinedRoomFlowNode @AssistedInject constructor( ) { data class Inputs( val roomId: RoomId, - val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(), + val initialElement: RoomNavigationTarget, ) : NodeInputs private val inputs: Inputs = inputs() @@ -106,7 +106,10 @@ class JoinedRoomFlowNode @AssistedInject constructor( val roomFlowNodeCallback = plugins() val awaitRoomState = loadingRoomStateStateFlow.value if (awaitRoomState is LoadingRoomState.Loaded) { - val inputs = JoinedRoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement) + val inputs = JoinedRoomLoadedFlowNode.Inputs( + room = awaitRoomState.room, + initialElement = inputs.initialElement + ) createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) } else { loadingNode(buildContext, this::navigateUp) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 5f9a6b6eb3..142b658e5e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -84,7 +84,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( data class Inputs( val room: MatrixRoom, - val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(), + val initialElement: RoomNavigationTarget, ) : NodeInputs private val inputs: Inputs = inputs() diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt index 2809fc95ee..93e727da75 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt @@ -27,6 +27,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper import com.google.common.truth.Truth.assertThat import io.element.android.appnav.di.RoomComponentFactory +import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint @@ -124,7 +125,7 @@ class JoinRoomLoadedFlowNodeTest { // GIVEN val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() - val inputs = JoinedRoomLoadedFlowNode.Inputs(room) + val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) val roomFlowNode = createJoinedRoomLoadedFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, @@ -146,7 +147,7 @@ class JoinRoomLoadedFlowNodeTest { val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() - val inputs = JoinedRoomLoadedFlowNode.Inputs(room) + val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) val roomFlowNode = createJoinedRoomLoadedFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, diff --git a/changelog.d/2759.feature b/changelog.d/2759.feature new file mode 100644 index 0000000000..bdd2c3ba77 --- /dev/null +++ b/changelog.d/2759.feature @@ -0,0 +1 @@ +Handle permalink navigation to Events. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt index 73ab29bfeb..0f6a6358d3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt @@ -32,7 +32,7 @@ class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { return object : MessagesEntryPoint.NodeBuilder { override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder { - plugins += MessagesNode.Inputs(focusedEventId = params.focusedEventId) + plugins += MessagesFlowNode.Inputs(focusedEventId = params.focusedEventId) return this } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index f92214b679..ca35ac7fee 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -54,6 +54,7 @@ import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.overlay.Overlay import io.element.android.libraries.architecture.overlay.operation.show import io.element.android.libraries.di.ApplicationContext @@ -81,7 +82,7 @@ class MessagesFlowNode @AssistedInject constructor( private val createPollEntryPoint: CreatePollEntryPoint, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Messages(plugins.filterIsInstance().firstOrNull()?.focusedEventId), + initialElement = NavTarget.Messages, savedStateMap = buildContext.savedStateMap, ), overlay = Overlay( @@ -91,15 +92,14 @@ class MessagesFlowNode @AssistedInject constructor( plugins = plugins ) { data class Inputs(val focusedEventId: EventId?) : NodeInputs + private val inputs = inputs() sealed interface NavTarget : Parcelable { @Parcelize data object Empty : NavTarget @Parcelize - data class Messages( - val focusedEventId: EventId? = null, - ) : NavTarget + data object Messages : NavTarget @Parcelize data class MediaViewer( @@ -191,10 +191,10 @@ class MessagesFlowNode @AssistedInject constructor( ElementCallActivity.start(context, inputs) } } - val params = MessagesNode.Inputs( - focusedEventId = navTarget.focusedEventId, + val inputs = MessagesNode.Inputs( + focusedEventId = inputs.focusedEventId, ) - createNode(buildContext, listOf(callback, params)) + createNode(buildContext, listOf(callback, inputs)) } is NavTarget.MediaViewer -> { val inputs = MediaViewerNode.Inputs( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 88472956c0..f3c3462cd8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -19,6 +19,11 @@ package io.element.android.features.messages.impl import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.lifecycle.subscribe @@ -30,12 +35,15 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.timeline.TimelineController +import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.androidutils.system.toast import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope @@ -64,13 +72,15 @@ class MessagesNode @AssistedInject constructor( private val permalinkParser: PermalinkParser, @ApplicationContext private val context: Context, + private val timelineController: TimelineController, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create(this) private val callback = plugins().firstOrNull() - // TODO Handle navigation to the Event data class Inputs(val focusedEventId: EventId?) : NodeInputs + private val inputs = inputs() + interface Callback : Plugin { fun onRoomDetailsClicked() fun onEventClicked(event: TimelineItem.Event): Boolean @@ -86,12 +96,14 @@ class MessagesNode @AssistedInject constructor( fun onJoinCallClicked(roomId: RoomId) } - init { + override fun onBuilt() { + super.onBuilt() lifecycle.subscribe( onCreate = { analyticsService.capture(room.toAnalyticsViewRoom()) }, onDestroy = { + timelineController.close() mediaPlayer.close() } ) @@ -116,6 +128,7 @@ class MessagesNode @AssistedInject constructor( private fun onLinkClicked( context: Context, url: String, + eventSink: (TimelineEvents) -> Unit, ) { when (val permalink = permalinkParser.parse(url)) { is PermalinkData.UserLink -> { @@ -124,7 +137,7 @@ class MessagesNode @AssistedInject constructor( callback?.onUserDataClicked(permalink.userId) } is PermalinkData.RoomLink -> { - handleRoomLinkClicked(permalink) + handleRoomLinkClicked(permalink, eventSink) } is PermalinkData.FallbackLink, is PermalinkData.RoomEmailInviteLink -> { @@ -133,11 +146,11 @@ class MessagesNode @AssistedInject constructor( } } - private fun handleRoomLinkClicked(roomLink: PermalinkData.RoomLink) { + private fun handleRoomLinkClicked(roomLink: PermalinkData.RoomLink, eventSink: (TimelineEvents) -> Unit) { if (room.matches(roomLink.roomIdOrAlias)) { - if (roomLink.eventId != null) { - // TODO Handle navigation to the Event - context.toast("TODO Handle navigation to the Event ${roomLink.eventId}") + val eventId = roomLink.eventId + if (eventId != null) { + eventSink(TimelineEvents.FocusOnEvent(eventId)) } else { // Click on the same room, ignore context.toast("Already viewing this room!") @@ -189,12 +202,23 @@ class MessagesNode @AssistedInject constructor( onEventClicked = this::onEventClicked, onPreviewAttachments = this::onPreviewAttachments, onUserDataClicked = this::onUserDataClicked, - onLinkClicked = { onLinkClicked(context, it) }, + onLinkClicked = { onLinkClicked(context, it, state.timelineState.eventSink) }, onSendLocationClicked = this::onSendLocationClicked, onCreatePollClicked = this::onCreatePollClicked, onJoinCallClicked = this::onJoinCallClicked, modifier = modifier, ) + + var focusedEventId by rememberSaveable { + mutableStateOf(inputs.focusedEventId) + } + LaunchedEffect(Unit) { + focusedEventId?.also { eventId -> + state.timelineState.eventSink(TimelineEvents.FocusOnEvent(eventId)) + } + // Reset the focused event id to null to avoid refocusing when restoring node. + focusedEventId = null + } } } } 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 2b74cf1072..5825d5acfb 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 @@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState +import io.element.android.features.messages.impl.timeline.TimelineController 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.TimelineState @@ -116,6 +117,7 @@ class MessagesPresenter @AssistedInject constructor( private val htmlConverterProvider: HtmlConverterProvider, @Assisted private val navigator: MessagesNavigator, private val buildMeta: BuildMeta, + private val timelineController: TimelineController, ) : Presenter { private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) @@ -185,10 +187,6 @@ class MessagesPresenter @AssistedInject constructor( val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() - LaunchedEffect(composerState.mode.relatedEventId) { - timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId)) - } - val enableTextFormatting by appPreferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) var enableVoiceMessages by remember { mutableStateOf(false) } @@ -290,8 +288,10 @@ class MessagesPresenter @AssistedInject constructor( emoji: String, eventId: EventId, ) = launch(dispatchers.io) { - room.toggleReaction(emoji, eventId) - .onFailure { Timber.e(it) } + timelineController.invokeOnCurrentTimeline { + toggleReaction(emoji, eventId) + .onFailure { Timber.e(it) } + } } private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState>) = launch(dispatchers.io) { 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 00f02aed5d..acd7e86d58 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 @@ -106,6 +106,8 @@ fun aMessagesState( voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(), timelineState: TimelineState = aTimelineState( timelineItems = aTimelineItemList(aTimelineItemTextContent()), + // Render a focused event for an event with sender information displayed + focusedEventIndex = 2, ), retrySendMenuState: RetrySendMenuState = aRetrySendMenuState(), readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(), 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 9cf4375769..597fd4b8c9 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 @@ -382,20 +382,19 @@ private fun MessagesViewContent( }, content = { paddingValues -> TimelineView( - modifier = Modifier.padding(paddingValues), state = state.timelineState, - roomName = state.roomName.dataOrNull(), typingNotificationState = state.typingNotificationState, - onMessageClicked = onMessageClicked, - onMessageLongClicked = onMessageLongClicked, onUserDataClicked = onUserDataClicked, onLinkClicked = onLinkClicked, + onMessageClicked = onMessageClicked, + onMessageLongClicked = onMessageLongClicked, onTimestampClicked = onTimestampClicked, + onSwipeToReply = onSwipeToReply, onReactionClicked = onReactionClicked, onReactionLongClicked = onReactionLongClicked, onMoreReactionsClicked = onMoreReactionsClicked, onReadReceiptClick = onReadReceiptClick, - onSwipeToReply = onSwipeToReply, + modifier = Modifier.padding(paddingValues), forceJumpToBottomVisibility = forceJumpToBottomVisibility, ) }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt index 367a071084..9cfdbcd14d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt @@ -29,7 +29,8 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.TimelineProvider +import io.element.android.libraries.matrix.api.timeline.getActiveTimeline import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope @@ -37,8 +38,8 @@ import kotlinx.coroutines.launch class ForwardMessagesPresenter @AssistedInject constructor( @Assisted eventId: String, - private val room: MatrixRoom, private val matrixCoroutineScope: CoroutineScope, + private val timelineProvider: TimelineProvider, ) : Presenter { private val eventId: EventId = EventId(eventId) @@ -79,7 +80,7 @@ class ForwardMessagesPresenter @AssistedInject constructor( isForwardMessagesState: MutableState>>, ) = launch { isForwardMessagesState.value = AsyncData.Loading() - room.forwardEvent(eventId, roomIds).fold( + timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).fold( { isForwardMessagesState.value = AsyncData.Success(roomIds) }, { isForwardMessagesState.value = AsyncData.Failure(it) } ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 04c182a566..30bdadcbe5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.mentions.MentionSuggestion import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor +import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -100,6 +101,7 @@ class MessageComposerPresenter @Inject constructor( private val permalinkParser: PermalinkParser, private val permalinkBuilder: PermalinkBuilder, permissionsPresenterFactory: PermissionsPresenter.Factory, + private val timelineController: TimelineController, ) : Presenter { private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA) private var pendingEvent: MessageComposerEvents? = null @@ -264,7 +266,9 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerMode.Quote -> null }.let { relatedEventId -> appCoroutineScope.launch { - room.enterSpecialMode(relatedEventId) + timelineController.invokeOnCurrentTimeline { + enterSpecialMode(relatedEventId) + } } } } @@ -386,16 +390,17 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerMode.Edit -> { val eventId = capturedMode.eventId val transactionId = capturedMode.transactionId - room.editMessage(eventId, transactionId, message.markdown, message.html, mentions) + timelineController.invokeOnCurrentTimeline { + editMessage(eventId, transactionId, message.markdown, message.html, mentions) + } } is MessageComposerMode.Quote -> TODO() - is MessageComposerMode.Reply -> room.replyMessage( - capturedMode.eventId, - message.markdown, - message.html, - mentions - ) + is MessageComposerMode.Reply -> { + timelineController.invokeOnCurrentTimeline { + replyMessage(capturedMode.eventId, message.markdown, message.html, mentions) + } + } } analyticsService.capture( Composer( 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 new file mode 100644 index 0000000000..1352124454 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024 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 + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.TimelineProvider +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import java.io.Closeable +import java.util.Optional +import javax.inject.Inject + +/** + * This controller is responsible of using the right timeline to display messages and make associated actions. + * It can be focused on the live timeline or on a detached timeline (focusing an unknown event). + */ +@SingleIn(RoomScope::class) +@ContributesBinding(RoomScope::class, boundType = TimelineProvider::class) +class TimelineController @Inject constructor( + private val room: MatrixRoom, +) : Closeable, TimelineProvider { + private val coroutineScope = CoroutineScope(SupervisorJob()) + + private val liveTimeline = flowOf(room.liveTimeline) + private val detachedTimeline = MutableStateFlow>(Optional.empty()) + + @OptIn(ExperimentalCoroutinesApi::class) + fun timelineItems(): Flow> { + return currentTimelineFlow.flatMapLatest { it.timelineItems } + } + + fun isLive(): Flow { + return detachedTimeline.map { !it.isPresent } + } + + suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) { + currentTimelineFlow.value.run { + block(this) + } + } + + suspend fun focusOnEvent(eventId: EventId): Result { + return room.timelineFocusedOnEvent(eventId) + .onFailure { + if (it is CancellationException) { + throw it + } + } + .map { newDetachedTimeline -> + detachedTimeline.getAndUpdate { current -> + if (current.isPresent) { + current.get().close() + } + Optional.of(newDetachedTimeline) + } + } + } + + /** + * Makes sure the controller is focused on the live timeline. + * This does close the detached timeline if any. + */ + fun focusOnLive() { + closeDetachedTimeline() + } + + private fun closeDetachedTimeline() { + detachedTimeline.getAndUpdate { + when { + it.isPresent -> { + it.get().close() + Optional.empty() + } + else -> Optional.empty() + } + } + } + + override fun close() { + coroutineScope.cancel() + closeDetachedTimeline() + } + + suspend fun paginate(direction: Timeline.PaginationDirection): Result { + return currentTimelineFlow.value.paginate(direction) + .onSuccess { hasReachedEnd -> + if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) { + focusOnLive() + } + } + } + + private val currentTimelineFlow = combine(liveTimeline, detachedTimeline) { live, detached -> + when { + detached.isPresent -> detached.get() + else -> live + } + }.stateIn(coroutineScope, SharingStarted.Eagerly, room.liveTimeline) + + override fun activeTimelineFlow(): StateFlow { + return currentTimelineFlow + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index cf02664a98..8a5d3cd275 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -17,17 +17,21 @@ package io.element.android.features.messages.impl.timeline import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.Timeline sealed interface TimelineEvents { - data object LoadMore : TimelineEvents - data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents data class OnScrollFinished(val firstIndex: Int) : TimelineEvents + data class FocusOnEvent(val eventId: EventId) : TimelineEvents + data object ClearFocusRequestState : TimelineEvents + data object JumpToLive : TimelineEvents /** * Events coming from a timeline item. */ sealed interface EventFromTimelineItem : TimelineEvents + data class LoadMore(val direction: Timeline.PaginationDirection) : EventFromTimelineItem + /** * Events coming from a poll item. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt new file mode 100644 index 0000000000..d11de93079 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 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 + +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.core.EventId +import timber.log.Timber +import javax.inject.Inject + +@SingleIn(RoomScope::class) +class TimelineItemIndexer @Inject constructor() { + private val timelineEventsIndexes = mutableMapOf() + + fun isKnown(eventId: EventId): Boolean { + return timelineEventsIndexes.containsKey(eventId).also { + Timber.d("$eventId isKnown = $it") + } + } + + fun indexOf(eventId: EventId): Int { + return (timelineEventsIndexes[eventId] ?: -1).also { + Timber.d("indexOf $eventId= $it") + } + } + + fun process(timelineItems: List) { + Timber.d("process ${timelineItems.size} items") + timelineEventsIndexes.clear() + timelineItems.forEachIndexed { index, timelineItem -> + when (timelineItem) { + is TimelineItem.Event -> { + processEvent(timelineItem, index) + } + is TimelineItem.GroupedEvents -> { + timelineItem.events.forEach { event -> + processEvent(event, index) + } + } + else -> Unit + } + } + } + + private fun processEvent(event: TimelineItem.Event, index: Int) { + if (event.eventId == null) return + timelineEventsIndexes[event.eventId] = index + } +} 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 3cd525c614..d6a02959c0 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 @@ -54,11 +54,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -private const val BACK_PAGINATION_EVENT_LIMIT = 20 -private const val BACK_PAGINATION_PAGE_SIZE = 50 - class TimelinePresenter @AssistedInject constructor( private val timelineItemsFactory: TimelineItemsFactory, + private val timelineItemIndexer: TimelineItemIndexer, private val room: MatrixRoom, private val dispatchers: CoroutineDispatchers, private val appScope: CoroutineScope, @@ -67,50 +65,62 @@ class TimelinePresenter @AssistedInject constructor( private val sendPollResponseAction: SendPollResponseAction, private val endPollAction: EndPollAction, private val sessionPreferencesStore: SessionPreferencesStore, + private val timelineController: TimelineController, ) : Presenter { @AssistedFactory interface Factory { fun create(navigator: MessagesNavigator): TimelinePresenter } - private val timeline = room.timeline - @Composable override fun present(): TimelineState { val localScope = rememberCoroutineScope() - val highlightedEventId: MutableState = rememberSaveable { + val focusedEventId: MutableState = rememberSaveable { mutableStateOf(null) } + val focusRequestState: MutableState = remember { + mutableStateOf(FocusRequestState.None) + } val lastReadReceiptId = rememberSaveable { mutableStateOf(null) } val timelineItems by timelineItemsFactory.collectItemsAsState() - val paginationState by timeline.paginationState.collectAsState() + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value) val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } - val newItemState = remember { mutableStateOf(NewEventState.None) } + + val newEventState = remember { mutableStateOf(NewEventState.None) } val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true) val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true) + val isLive by timelineController.isLive().collectAsState(initial = true) fun handleEvents(event: TimelineEvents) { when (event) { - TimelineEvents.LoadMore -> localScope.paginateBackwards() - is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId - is TimelineEvents.OnScrollFinished -> { - if (event.firstIndex == 0) { - newItemState.value = NewEventState.None + is TimelineEvents.LoadMore -> { + localScope.launch { + timelineController.paginate(direction = event.direction) + } + } + is TimelineEvents.OnScrollFinished -> { + if (isLive) { + if (event.firstIndex == 0) { + newEventState.value = NewEventState.None + } + println("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}") + appScope.sendReadReceiptIfNeeded( + firstVisibleIndex = event.firstIndex, + timelineItems = timelineItems, + lastReadReceiptId = lastReadReceiptId, + readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE, + ) + } else { + newEventState.value = NewEventState.None } - appScope.sendReadReceiptIfNeeded( - firstVisibleIndex = event.firstIndex, - timelineItems = timelineItems, - lastReadReceiptId = lastReadReceiptId, - readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE, - ) } is TimelineEvents.PollAnswerSelected -> appScope.launch { sendPollResponseAction.execute( @@ -123,28 +133,58 @@ class TimelinePresenter @AssistedInject constructor( pollStartId = event.pollStartId, ) } - is TimelineEvents.PollEditClicked -> + is TimelineEvents.PollEditClicked -> { navigator.onEditPollClicked(event.pollStartId) + } + is TimelineEvents.FocusOnEvent -> localScope.launch { + focusedEventId.value = event.eventId + if (timelineItemIndexer.isKnown(event.eventId)) { + val index = timelineItemIndexer.indexOf(event.eventId) + focusRequestState.value = FocusRequestState.Cached(index) + } else { + focusRequestState.value = FocusRequestState.Fetching + timelineController.focusOnEvent(event.eventId) + .fold( + onSuccess = { + focusRequestState.value = FocusRequestState.Fetched + }, + onFailure = { + focusRequestState.value = FocusRequestState.Failure(it) + } + ) + } + } + is TimelineEvents.ClearFocusRequestState -> { + focusRequestState.value = FocusRequestState.None + } + is TimelineEvents.JumpToLive -> { + timelineController.focusOnLive() + } } } LaunchedEffect(timelineItems.size) { - computeNewItemState(timelineItems, prevMostRecentItemId, newItemState) + computeNewItemState(timelineItems, prevMostRecentItemId, newEventState) + } + + LaunchedEffect(timelineItems.size, focusRequestState.value, focusedEventId.value) { + val currentFocusedEventId = focusedEventId.value + if (focusRequestState.value is FocusRequestState.Fetched && currentFocusedEventId != null) { + if (timelineItemIndexer.isKnown(currentFocusedEventId)) { + val index = timelineItemIndexer.indexOf(currentFocusedEventId) + focusRequestState.value = FocusRequestState.Cached(index) + } + } } LaunchedEffect(Unit) { - combine(timeline.timelineItems, room.membersStateFlow) { items, membersState -> + combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState -> timelineItemsFactory.replaceWith( timelineItems = items, roomMembers = membersState.roomMembers().orEmpty() ) items } - .onEach { timelineItems -> - if (timelineItems.isEmpty()) { - paginateBackwards() - } - } .onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem) .launchIn(this) } @@ -152,6 +192,7 @@ class TimelinePresenter @AssistedInject constructor( val timelineRoomInfo by remember { derivedStateOf { TimelineRoomInfo( + name = room.displayName, isDm = room.isDm, userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = userHasPermissionToSendReaction, @@ -160,11 +201,12 @@ class TimelinePresenter @AssistedInject constructor( } return TimelineState( timelineRoomInfo = timelineRoomInfo, - highlightedEventId = highlightedEventId.value, - paginationState = paginationState, timelineItems = timelineItems, renderReadReceipts = renderReadReceipts, - newEventState = newItemState.value, + newEventState = newEventState.value, + isLive = isLive, + focusedEventId = focusedEventId.value, + focusRequestState = focusRequestState.value, eventSink = { handleEvents(it) } ) } @@ -190,6 +232,7 @@ class TimelinePresenter @AssistedInject constructor( newMostRecentItem is TimelineItem.Event && newMostRecentItem.origin != TimelineItemEventOrigin.PAGINATION && newMostRecentItemId != prevMostRecentItemIdValue + if (hasNewEvent) { val newMostRecentEvent = newMostRecentItem as? TimelineItem.Event // Scroll to bottom if the new event is from me, even if sent from another device @@ -217,7 +260,7 @@ class TimelinePresenter @AssistedInject constructor( val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) if (eventId != null && eventId != lastReadReceiptId.value) { lastReadReceiptId.value = eventId - timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) + room.liveTimeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) } } } @@ -231,8 +274,4 @@ class TimelinePresenter @AssistedInject constructor( } return null } - - private fun CoroutineScope.paginateBackwards() = launch { - timeline.paginateBackwards(BACK_PAGINATION_EVENT_LIMIT, BACK_PAGINATION_PAGE_SIZE) - } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 4e2f9b8d42..c32dbad723 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import kotlinx.collections.immutable.ImmutableList @Immutable @@ -28,15 +27,28 @@ data class TimelineState( val timelineItems: ImmutableList, val timelineRoomInfo: TimelineRoomInfo, val renderReadReceipts: Boolean, - val highlightedEventId: EventId?, - val paginationState: MatrixTimeline.PaginationState, val newEventState: NewEventState, - val eventSink: (TimelineEvents) -> Unit -) + val isLive: Boolean, + val focusedEventId: EventId?, + val focusRequestState: FocusRequestState, + val eventSink: (TimelineEvents) -> Unit, +) { + val hasAnyEvent = timelineItems.any { it is TimelineItem.Event } +} + +@Immutable +sealed interface FocusRequestState { + data object None : FocusRequestState + data class Cached(val index: Int) : FocusRequestState + data object Fetching : FocusRequestState + data object Fetched : FocusRequestState + data class Failure(val throwable: Throwable) : FocusRequestState +} @Immutable data class TimelineRoomInfo( val isDm: Boolean, + val name: String?, val userHasPermissionToSendMessage: Boolean, val userHasPermissionToSendReaction: Boolean, ) 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 f2d2d8624c..b9e417b188 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 @@ -35,7 +35,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import kotlinx.collections.immutable.ImmutableList @@ -47,32 +46,22 @@ import kotlin.random.Random fun aTimelineState( timelineItems: ImmutableList = persistentListOf(), - paginationState: MatrixTimeline.PaginationState = aPaginationState(), renderReadReceipts: Boolean = false, timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), + focusedEventIndex: Int = -1, + isLive: Boolean = true, eventSink: (TimelineEvents) -> Unit = {}, ) = TimelineState( timelineItems = timelineItems, timelineRoomInfo = timelineRoomInfo, - paginationState = paginationState, renderReadReceipts = renderReadReceipts, - highlightedEventId = null, newEventState = NewEventState.None, + isLive = isLive, + focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId, + focusRequestState = FocusRequestState.None, eventSink = eventSink, ) -fun aPaginationState( - isBackPaginating: Boolean = false, - hasMoreToLoadBackwards: Boolean = true, - beginningOfRoomReached: Boolean = false, -): MatrixTimeline.PaginationState { - return MatrixTimeline.PaginationState( - isBackPaginating = isBackPaginating, - hasMoreToLoadBackwards = hasMoreToLoadBackwards, - beginningOfRoomReached = beginningOfRoomReached, - ) -} - internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList { return persistentListOf( // 3 items (First Middle Last) with isMine = false @@ -235,10 +224,12 @@ internal fun aGroupedEvents( } internal fun aTimelineRoomInfo( + name: String = "Room name", isDm: Boolean = false, userHasPermissionToSendMessage: Boolean = true, ) = TimelineRoomInfo( isDm = isDm, + name = name, userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = true, ) 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 56a2676f43..3483c27f85 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 @@ -55,10 +55,9 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.components.TimelineItemRow -import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView -import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.focus.FocusRequestStateView import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent @@ -74,12 +73,12 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch +import kotlin.math.abs @Composable fun TimelineView( state: TimelineState, typingNotificationState: TypingNotificationState, - roomName: String?, onUserDataClicked: (UserId) -> Unit, onLinkClicked: (String) -> Unit, onMessageClicked: (TimelineItem.Event) -> Unit, @@ -93,8 +92,8 @@ fun TimelineView( modifier: Modifier = Modifier, forceJumpToBottomVisibility: Boolean = false ) { - fun onReachedLoadMore() { - state.eventSink(TimelineEvents.LoadMore) + fun clearFocusRequestState() { + state.eventSink(TimelineEvents.ClearFocusRequestState) } fun onScrollFinishedAt(firstVisibleIndex: Int) { @@ -109,9 +108,8 @@ fun TimelineView( accessibilityManager.isTouchExplorationEnabled.not() } - @Suppress("UNUSED_PARAMETER") fun inReplyToClicked(eventId: EventId) { - // TODO implement this logic once we have support to 'jump to event X' in sliding sync + state.eventSink(TimelineEvents.FocusOnEvent(eventId)) } // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms @@ -123,8 +121,10 @@ fun TimelineView( reverseLayout = useReverseLayout, contentPadding = PaddingValues(vertical = 8.dp), ) { - item { - TypingNotificationView(state = typingNotificationState) + if (state.isLive) { + item { + TypingNotificationView(state = typingNotificationState) + } } items( items = state.timelineItems, @@ -137,7 +137,7 @@ fun TimelineView( renderReadReceipts = state.renderReadReceipts, isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && state.timelineItems.first().identifier() == timelineItem.identifier(), - highlightedItem = state.highlightedEventId?.value, + focusedEventId = state.focusedEventId, onClick = onMessageClicked, onLongClick = onMessageLongClicked, onUserDataClick = onUserDataClicked, @@ -152,28 +152,23 @@ fun TimelineView( onSwipeToReply = onSwipeToReply, ) } - if (state.paginationState.hasMoreToLoadBackwards) { - // Do not use key parameter to avoid wrong positioning - item(contentType = "TimelineLoadingMoreIndicator") { - TimelineLoadingMoreIndicator() - LaunchedEffect(Unit) { - onReachedLoadMore() - } - } - } - if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDm) { - item(contentType = "BeginningOfRoomReached") { - TimelineItemRoomBeginningView(roomName = roomName) - } - } } + FocusRequestStateView( + focusRequestState = state.focusRequestState, + onClearFocusRequestState = ::clearFocusRequestState + ) + TimelineScrollHelper( - isTimelineEmpty = state.timelineItems.isEmpty(), + hasAnyEvent = state.hasAnyEvent, lazyListState = lazyListState, forceJumpToBottomVisibility = forceJumpToBottomVisibility, newEventState = state.newEventState, - onScrollFinishedAt = ::onScrollFinishedAt + isLive = state.isLive, + focusRequestState = state.focusRequestState, + onScrollFinishedAt = ::onScrollFinishedAt, + onClearFocusRequestState = ::clearFocusRequestState, + onJumpToLive = { state.eventSink(TimelineEvents.JumpToLive) }, ) } } @@ -181,17 +176,21 @@ fun TimelineView( @Composable private fun BoxScope.TimelineScrollHelper( - isTimelineEmpty: Boolean, + hasAnyEvent: Boolean, lazyListState: LazyListState, newEventState: NewEventState, + isLive: Boolean, forceJumpToBottomVisibility: Boolean, + focusRequestState: FocusRequestState, + onClearFocusRequestState: () -> Unit, onScrollFinishedAt: (Int) -> Unit, + onJumpToLive: () -> Unit, ) { val coroutineScope = rememberCoroutineScope() val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } } val canAutoScroll by remember { derivedStateOf { - lazyListState.firstVisibleItemIndex < 3 + lazyListState.firstVisibleItemIndex < 3 && isLive } } @@ -205,16 +204,36 @@ private fun BoxScope.TimelineScrollHelper( } } + fun jumpToBottom() { + if (isLive) { + scrollToBottom() + } else { + onJumpToLive() + } + } + + val latestOnClearFocusRequestState by rememberUpdatedState(onClearFocusRequestState) + LaunchedEffect(focusRequestState) { + if (focusRequestState is FocusRequestState.Cached) { + if (abs(lazyListState.firstVisibleItemIndex - focusRequestState.index) < 10) { + lazyListState.animateScrollToItem(focusRequestState.index) + } else { + lazyListState.scrollToItem(focusRequestState.index) + } + latestOnClearFocusRequestState() + } + } + LaunchedEffect(canAutoScroll, newEventState) { - val shouldAutoScroll = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe) - if (shouldAutoScroll) { + val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe) + if (shouldScrollToBottom) { scrollToBottom() } } val latestOnScrollFinishedAt by rememberUpdatedState(onScrollFinishedAt) - LaunchedEffect(isScrollFinished, isTimelineEmpty) { - if (isScrollFinished && !isTimelineEmpty) { + LaunchedEffect(isScrollFinished, hasAnyEvent) { + if (isScrollFinished && hasAnyEvent) { // Notify the parent composable about the first visible item index when scrolling finishes latestOnScrollFinishedAt(lazyListState.firstVisibleItemIndex) } @@ -222,11 +241,11 @@ private fun BoxScope.TimelineScrollHelper( JumpToBottomButton( // Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered - isVisible = !canAutoScroll || forceJumpToBottomVisibility, + isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive, modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 24.dp, bottom = 12.dp), - onClick = ::scrollToBottom, + onClick = { jumpToBottom() }, ) } @@ -271,18 +290,20 @@ internal fun TimelineViewPreview( LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(), ) { TimelineView( - state = aTimelineState(timelineItems), - roomName = null, + state = aTimelineState( + timelineItems = timelineItems, + focusedEventIndex = 0, + ), typingNotificationState = aTypingNotificationState(), - onMessageClicked = {}, - onTimestampClicked = {}, onUserDataClicked = {}, onLinkClicked = {}, + onMessageClicked = {}, onMessageLongClicked = {}, + onTimestampClicked = {}, + onSwipeToReply = {}, onReactionClicked = { _, _ -> }, onReactionLongClicked = { _, _ -> }, onMoreReactionsClicked = {}, - onSwipeToReply = {}, onReadReceiptClick = {}, forceJumpToBottomVisibility = true, ) 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 716474de26..87dd60b8a7 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 @@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -93,7 +92,9 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo +import io.element.android.features.messages.impl.timeline.model.eventId import io.element.android.features.messages.impl.timeline.model.metadata +import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -146,7 +147,7 @@ fun TimelineItemEventRow( } fun inReplyToClicked() { - val inReplyToEventId = event.inReplyTo?.eventId ?: return + val inReplyToEventId = event.inReplyTo?.eventId() ?: return inReplyToClick(inReplyToEventId) } @@ -417,7 +418,6 @@ private fun MessageSenderInformation( private fun MessageEventBubbleContent( event: TimelineItem.Event, onMessageLongClick: () -> Unit, - @Suppress("UNUSED_PARAMETER") inReplyToClick: () -> Unit, onTimestampClicked: () -> Unit, onLinkClicked: (String) -> Unit, @@ -437,7 +437,7 @@ private fun MessageEventBubbleContent( ) { Row( modifier = modifier, - horizontalArrangement = spacedBy(4.dp, Alignment.Start), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -565,16 +565,30 @@ private fun MessageEventBubbleContent( } val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> val topPadding = if (showThreadDecoration) 0.dp else 8.dp - ReplyToContent( - senderId = inReplyTo.senderId, - senderProfile = inReplyTo.senderProfile, - metadata = inReplyTo.metadata(), - modifier = Modifier - .padding(top = topPadding, start = 8.dp, end = 8.dp) - .clip(RoundedCornerShape(6.dp)) + val inReplyToModifier = Modifier + .padding(top = topPadding, start = 8.dp, end = 8.dp) + .clip(RoundedCornerShape(6.dp)) // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent -// .clickable(enabled = true, onClick = inReplyToClick) - ) + .clickable(onClick = inReplyToClick) + when (inReplyTo) { + is InReplyToDetails.Ready -> { + ReplyToContent( + senderId = inReplyTo.senderId, + senderProfile = inReplyTo.senderProfile, + metadata = inReplyTo.metadata(), + modifier = inReplyToModifier, + ) + } + is InReplyToDetails.Error -> + ReplyToErrorContent( + data = inReplyTo, + modifier = inReplyToModifier, + ) + is InReplyToDetails.Loading -> + ReplyToLoadingContent( + modifier = inReplyToModifier, + ) + } } if (inReplyToDetails != null) { // Use SubComposeLayout only if necessary as it can have consequences on the performance. @@ -584,7 +598,7 @@ private fun MessageEventBubbleContent( contentWithTimestamp() } } else { - Column(modifier = modifier, verticalArrangement = spacedBy(8.dp)) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { threadDecoration() contentWithTimestamp() } @@ -652,6 +666,44 @@ private fun ReplyToContent( } } +@Composable +private fun ReplyToLoadingContent( + modifier: Modifier = Modifier, +) { + val paddings = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + Row( + modifier + .background(MaterialTheme.colorScheme.surface) + .padding(paddings) + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + PlaceholderAtom(width = 80.dp, height = 12.dp) + PlaceholderAtom(width = 140.dp, height = 14.dp) + } + } +} + +@Composable +private fun ReplyToErrorContent( + data: InReplyToDetails.Error, + modifier: Modifier = Modifier, +) { + val paddings = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + Row( + modifier + .background(MaterialTheme.colorScheme.surface) + .padding(paddings) + ) { + Text( + text = data.message, + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.error, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } +} + @Composable private fun ReplyToContentText(metadata: InReplyToMetadata?) { val text = when (metadata) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt new file mode 100644 index 0000000000..2231af606d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 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 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.messages.impl.timeline.model.InReplyToDetails +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.core.EventId + +@PreviewsDayNight +@Composable +internal fun TimelineItemEventRowWithReplyOtherPreview( + @PreviewParameter(InReplyToDetailsOtherProvider::class) inReplyToDetails: InReplyToDetails, +) = ElementPreview { + TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) +} + +class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() { + override val values: Sequence + get() = sequenceOf( + InReplyToDetails.Loading(eventId = EventId("\$anEventId")), + InReplyToDetails.Error(eventId = EventId("\$anEventId"), message = "An error message."), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt index 8c59c3c07a..b1500dac2c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt @@ -170,7 +170,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider protected fun aInReplyToDetails( eventContent: EventContent, displayNameAmbiguous: Boolean = false, - ) = InReplyToDetails( + ) = InReplyToDetails.Ready( eventId = EventId("\$event"), eventContent = eventContent, senderId = UserId("@Sender:domain"), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index a5d42d5dbf..2dcc0c1b55 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -43,7 +43,7 @@ fun TimelineItemGroupedEventsRow( timelineRoomInfo: TimelineRoomInfo, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, - highlightedItem: String?, + focusedEventId: EventId?, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, @@ -68,7 +68,7 @@ fun TimelineItemGroupedEventsRow( onExpandGroupClick = ::onExpandGroupClick, timelineItem = timelineItem, timelineRoomInfo = timelineRoomInfo, - highlightedItem = highlightedItem, + focusedEventId = focusedEventId, renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, onClick = onClick, @@ -92,7 +92,7 @@ private fun TimelineItemGroupedEventsRowContent( onExpandGroupClick: () -> Unit, timelineItem: TimelineItem.GroupedEvents, timelineRoomInfo: TimelineRoomInfo, - highlightedItem: String?, + focusedEventId: EventId?, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, onClick: (TimelineItem.Event) -> Unit, @@ -116,7 +116,7 @@ private fun TimelineItemGroupedEventsRowContent( timelineItem.events.size ), isExpanded = isExpanded, - isHighlighted = !isExpanded && timelineItem.events.any { it.identifier() == highlightedItem }, + isHighlighted = !isExpanded && timelineItem.events.any { it.isEvent(focusedEventId) }, onClick = onExpandGroupClick, ) if (isExpanded) { @@ -127,7 +127,7 @@ private fun TimelineItemGroupedEventsRowContent( timelineRoomInfo = timelineRoomInfo, renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, - highlightedItem = highlightedItem, + focusedEventId = focusedEventId, onClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, @@ -160,12 +160,13 @@ private fun TimelineItemGroupedEventsRowContent( @PreviewsDayNight @Composable internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPreview { + val events = aGroupedEvents(withReadReceipts = true) TimelineItemGroupedEventsRowContent( isExpanded = true, onExpandGroupClick = {}, - timelineItem = aGroupedEvents(withReadReceipts = true), + timelineItem = events, timelineRoomInfo = aTimelineRoomInfo(), - highlightedItem = null, + focusedEventId = events.events.first().eventId, renderReadReceipts = true, isLastOutgoingMessage = false, onClick = {}, @@ -190,7 +191,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi onExpandGroupClick = {}, timelineItem = aGroupedEvents(withReadReceipts = true), timelineRoomInfo = aTimelineRoomInfo(), - highlightedItem = null, + focusedEventId = null, renderReadReceipts = true, isLastOutgoingMessage = false, onClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 89b223dafe..4193a9f131 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -16,13 +16,24 @@ package io.element.android.features.messages.impl.timeline.components +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.libraries.designsystem.text.toPx +import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -32,7 +43,7 @@ internal fun TimelineItemRow( timelineRoomInfo: TimelineRoomInfo, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, - highlightedItem: String?, + focusedEventId: EventId?, onUserDataClick: (UserId) -> Unit, onLinkClicked: (String) -> Unit, onClick: (TimelineItem.Event) -> Unit, @@ -47,69 +58,111 @@ internal fun TimelineItemRow( eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier ) { - when (timelineItem) { - is TimelineItem.Virtual -> { - TimelineItemVirtualRow( - virtual = timelineItem, - modifier = modifier, - ) + val backgroundModifier = if (timelineItem.isEvent(focusedEventId)) { + val focusedEventOffset = if ((timelineItem as? TimelineItem.Event)?.showSenderInformation == true) { + 14.dp + } else { + 2.dp } - is TimelineItem.Event -> { - if (timelineItem.content is TimelineItemStateContent || timelineItem.content is TimelineItemLegacyCallInviteContent) { - TimelineItemStateEventRow( - event = timelineItem, - renderReadReceipts = renderReadReceipts, - isLastOutgoingMessage = isLastOutgoingMessage, - isHighlighted = highlightedItem == timelineItem.identifier(), - onClick = { onClick(timelineItem) }, - onReadReceiptsClick = onReadReceiptClick, - onLongClick = { onLongClick(timelineItem) }, + Modifier.focusedEvent(focusedEventOffset) + } else { + Modifier + } + Box(modifier = modifier.then(backgroundModifier)) { + when (timelineItem) { + is TimelineItem.Virtual -> { + TimelineItemVirtualRow( + virtual = timelineItem, + timelineRoomInfo = timelineRoomInfo, eventSink = eventSink, - modifier = modifier, ) - } else { - TimelineItemEventRow( - event = timelineItem, + } + is TimelineItem.Event -> { + if (timelineItem.content is TimelineItemStateContent || timelineItem.content is TimelineItemLegacyCallInviteContent) { + TimelineItemStateEventRow( + event = timelineItem, + renderReadReceipts = renderReadReceipts, + isLastOutgoingMessage = isLastOutgoingMessage, + isHighlighted = timelineItem.isEvent(focusedEventId), + onClick = { onClick(timelineItem) }, + onReadReceiptsClick = onReadReceiptClick, + onLongClick = { onLongClick(timelineItem) }, + eventSink = eventSink, + ) + } else { + TimelineItemEventRow( + event = timelineItem, + timelineRoomInfo = timelineRoomInfo, + renderReadReceipts = renderReadReceipts, + isLastOutgoingMessage = isLastOutgoingMessage, + isHighlighted = timelineItem.isEvent(focusedEventId), + onClick = { onClick(timelineItem) }, + onLongClick = { onLongClick(timelineItem) }, + onUserDataClick = onUserDataClick, + onLinkClicked = onLinkClicked, + inReplyToClick = inReplyToClick, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + onTimestampClicked = onTimestampClicked, + onSwipeToReply = { onSwipeToReply(timelineItem) }, + eventSink = eventSink, + ) + } + } + is TimelineItem.GroupedEvents -> { + TimelineItemGroupedEventsRow( + timelineItem = timelineItem, timelineRoomInfo = timelineRoomInfo, renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, - isHighlighted = highlightedItem == timelineItem.identifier(), - onClick = { onClick(timelineItem) }, - onLongClick = { onLongClick(timelineItem) }, + focusedEventId = focusedEventId, + onClick = onClick, + onLongClick = onLongClick, + inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClicked = onLinkClicked, - inReplyToClick = inReplyToClick, + onTimestampClicked = onTimestampClicked, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, - onTimestampClicked = onTimestampClicked, - onSwipeToReply = { onSwipeToReply(timelineItem) }, eventSink = eventSink, - modifier = modifier, ) } } - is TimelineItem.GroupedEvents -> { - TimelineItemGroupedEventsRow( - timelineItem = timelineItem, - timelineRoomInfo = timelineRoomInfo, - renderReadReceipts = renderReadReceipts, - isLastOutgoingMessage = isLastOutgoingMessage, - highlightedItem = highlightedItem, - onClick = onClick, - onLongClick = onLongClick, - inReplyToClick = inReplyToClick, - onUserDataClick = onUserDataClick, - onLinkClicked = onLinkClicked, - onTimestampClicked = onTimestampClicked, - onReactionClick = onReactionClick, - onReactionLongClick = onReactionLongClick, - onMoreReactionsClick = onMoreReactionsClick, - onReadReceiptClick = onReadReceiptClick, - eventSink = eventSink, - modifier = modifier, - ) - } } } + +@Suppress("ModifierComposable") +@Composable +private fun Modifier.focusedEvent( + focusedEventOffset: Dp +): Modifier { + val highlightedLineColor = ElementTheme.colors.textActionAccent + val gradientColors = listOf( + ElementTheme.colors.highlightedMessageBackgroundColor, + ElementTheme.materialColors.background + ) + val verticalOffset = focusedEventOffset.toPx() + val verticalRatio = 0.7f + return drawWithCache { + val brush = Brush.verticalGradient( + colors = gradientColors, + endY = size.height * verticalRatio, + ) + onDrawBehind { + drawRect( + brush, + topLeft = Offset(0f, verticalOffset), + size = Size(size.width, size.height * verticalRatio) + ) + drawLine( + highlightedLineColor, + start = Offset(0f, verticalOffset), + end = Offset(size.width, verticalOffset) + ) + } + }.padding(top = 4.dp) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt index 306c11aaba..d56fe00540 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt @@ -16,24 +16,51 @@ package io.element.android.features.messages.impl.timeline.components +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier +import io.element.android.features.messages.impl.timeline.TimelineEvents +import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemReadMarkerView +import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView +import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel @Composable fun TimelineItemVirtualRow( virtual: TimelineItem.Virtual, + timelineRoomInfo: TimelineRoomInfo, + eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier ) { - when (virtual.model) { - is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier) - TimelineItemReadMarkerModel -> TimelineItemReadMarkerView() - is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier) + Box(modifier = modifier) { + when (virtual.model) { + is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model) + TimelineItemReadMarkerModel -> TimelineItemReadMarkerView() + is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView() + TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name) + is TimelineItemLoadingIndicatorModel -> { + TimelineLoadingMoreIndicator(virtual.model.direction) + val latestEventSink by rememberUpdatedState(eventSink) + LaunchedEffect(virtual.model.timestamp) { + latestEventSink(TimelineEvents.LoadMore(virtual.model.direction)) + } + } + is TimelineItemLastForwardIndicatorModel -> { + Spacer(modifier = Modifier) + } + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineLoadingMoreIndicator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineLoadingMoreIndicator.kt index 23cc516156..1719956cdb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineLoadingMoreIndicator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineLoadingMoreIndicator.kt @@ -16,10 +16,12 @@ package io.element.android.features.messages.impl.timeline.components.virtual +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -27,24 +29,45 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator +import io.element.android.libraries.matrix.api.timeline.Timeline @Composable -internal fun TimelineLoadingMoreIndicator(modifier: Modifier = Modifier) { +internal fun TimelineLoadingMoreIndicator( + direction: Timeline.PaginationDirection, + modifier: Modifier = Modifier +) { Box( - modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(8.dp), + modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator( - strokeWidth = 2.dp, - ) + when (direction) { + Timeline.PaginationDirection.FORWARDS -> { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp) + .height(1.dp) + ) + } + Timeline.PaginationDirection.BACKWARDS -> { + CircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } } } @PreviewsDayNight @Composable internal fun TimelineLoadingMoreIndicatorPreview() = ElementPreview { - TimelineLoadingMoreIndicator() + Column( + modifier = Modifier.padding(vertical = 2.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + TimelineLoadingMoreIndicator(Timeline.PaginationDirection.BACKWARDS) + TimelineLoadingMoreIndicator(Timeline.PaginationDirection.FORWARDS) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index 29ea3abcfc..d3e21d0042 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.factories import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import io.element.android.features.messages.impl.timeline.TimelineItemIndexer import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory @@ -43,9 +44,9 @@ class TimelineItemsFactory @Inject constructor( private val eventItemFactory: TimelineItemEventFactory, private val virtualItemFactory: TimelineItemVirtualFactory, private val timelineItemGrouper: TimelineItemGrouper, + private val timelineItemIndexer: TimelineItemIndexer, ) { private val timelineItems = MutableStateFlow(persistentListOf()) - private val lock = Mutex() private val diffCache = MutableListDiffCache() private val diffCacheUpdater = DiffCacheUpdater( @@ -100,6 +101,7 @@ class TimelineItemsFactory @Inject constructor( } } val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList() + timelineItemIndexer.process(result) this.timelineItems.emit(result) } @@ -108,13 +110,13 @@ class TimelineItemsFactory @Inject constructor( index: Int, roomMembers: List, ): TimelineItem? { - val timelineItemState = + val timelineItem = when (val currentTimelineItem = timelineItems[index]) { is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers) is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem) MatrixTimelineItem.Other -> null } - diffCache[index] = timelineItemState - return timelineItemState + diffCache[index] = timelineItem + return timelineItem } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt index 64d08acacb..2fe782b960 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt @@ -18,7 +18,10 @@ package io.element.android.features.messages.impl.timeline.factories.virtual import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem @@ -41,6 +44,12 @@ class TimelineItemVirtualFactory @Inject constructor( is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner) is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel + is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel + is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel( + direction = inner.direction, + timestamp = inner.timestamp + ) + is VirtualTimelineItem.LastForwardIndicator -> TimelineItemLastForwardIndicatorModel } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt new file mode 100644 index 0000000000..cb5b4a10f2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 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.focus + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.FocusRequestState +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.errors.FocusEventException + +open class FocusRequestStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + FocusRequestState.Fetching, + FocusRequestState.Failure( + FocusEventException.EventNotFound( + eventId = EventId("\$anEventId"), + ) + ), + FocusRequestState.Failure( + FocusEventException.InvalidEventId( + eventId = "invalid", + err = "An error" + ) + ), + FocusRequestState.Failure( + FocusEventException.Other( + msg = "An error" + ) + ), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt new file mode 100644 index 0000000000..4a4381d269 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 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.focus + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.messages.impl.timeline.FocusRequestState +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.room.errors.FocusEventException +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun FocusRequestStateView( + focusRequestState: FocusRequestState, + onClearFocusRequestState: () -> Unit, + modifier: Modifier = Modifier, +) { + when (focusRequestState) { + is FocusRequestState.Failure -> { + val errorMessage = when (focusRequestState.throwable) { + is FocusEventException.EventNotFound, + is FocusEventException.InvalidEventId -> stringResource(id = CommonStrings.error_message_not_found) + is FocusEventException.Other -> stringResource(id = CommonStrings.error_unknown) + else -> stringResource(id = CommonStrings.error_unknown) + } + ErrorDialog( + content = errorMessage, + onDismiss = onClearFocusRequestState, + modifier = modifier, + ) + } + FocusRequestState.Fetching -> { + ProgressDialog(modifier = modifier, onDismissRequest = onClearFocusRequestState) + } + else -> Unit + } +} + +@PreviewsDayNight +@Composable +internal fun FocusRequestStateViewPreview( + @PreviewParameter(FocusRequestStateProvider::class) state: FocusRequestState, +) = ElementPreview { + FocusRequestStateView( + focusRequestState = state, + onClearFocusRequestState = {}, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt index 83627b5b25..5317c3231e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.timeline.model +import androidx.compose.runtime.Immutable 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.permalink.PermalinkParser @@ -27,18 +28,30 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerConten import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.ui.messages.toPlainText -data class InReplyToDetails( - val eventId: EventId, - val senderId: UserId, - val senderProfile: ProfileTimelineDetails, - val eventContent: EventContent?, - val textContent: String?, -) +@Immutable +sealed interface InReplyToDetails { + data class Ready( + val eventId: EventId, + val senderId: UserId, + val senderProfile: ProfileTimelineDetails, + val eventContent: EventContent?, + val textContent: String?, + ) : InReplyToDetails + + data class Loading(val eventId: EventId) : InReplyToDetails + data class Error(val eventId: EventId, val message: String) : InReplyToDetails +} + +fun InReplyToDetails.eventId() = when (this) { + is InReplyToDetails.Ready -> eventId + is InReplyToDetails.Loading -> eventId + is InReplyToDetails.Error -> eventId +} fun InReplyTo.map( permalinkParser: PermalinkParser, ) = when (this) { - is InReplyTo.Ready -> InReplyToDetails( + is InReplyTo.Ready -> InReplyToDetails.Ready( eventId = eventId, senderId = senderId, senderProfile = senderProfile, @@ -55,5 +68,7 @@ fun InReplyTo.map( else -> null } ) - else -> null + is InReplyTo.Error -> InReplyToDetails.Error(eventId, message) + is InReplyTo.NotLoaded -> InReplyToDetails.Loading(eventId) + is InReplyTo.Pending -> InReplyToDetails.Loading(eventId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt index fb63912b69..ee6589f046 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt @@ -66,7 +66,7 @@ internal sealed interface InReplyToMetadata { * Metadata can be either a thumbnail with a text OR just a text. */ @Composable -internal fun InReplyToDetails.metadata(): InReplyToMetadata? = when (eventContent) { +internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (eventContent) { is MessageContent -> when (val type = eventContent.type) { is ImageMessageType -> InReplyToMetadata.Thumbnail( AttachmentThumbnailInfo( 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 c42869af7a..e948484e5e 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 @@ -40,6 +40,14 @@ sealed interface TimelineItem { is GroupedEvents -> id } + fun isEvent(eventId: EventId?): Boolean { + if (eventId == null) return false + return when (this) { + is Event -> this.eventId == eventId + else -> false + } + } + fun contentType(): String = when (this) { is Event -> content.type is Virtual -> model.type diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLastForwardIndicatorModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLastForwardIndicatorModel.kt new file mode 100644 index 0000000000..b0b6376f50 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLastForwardIndicatorModel.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 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.model.virtual + +data object TimelineItemLastForwardIndicatorModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemLastForwardIndicatorModel" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingIndicatorModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingIndicatorModel.kt new file mode 100644 index 0000000000..da022d6d59 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingIndicatorModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 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.model.virtual + +import io.element.android.libraries.matrix.api.timeline.Timeline + +data class TimelineItemLoadingIndicatorModel( + val direction: Timeline.PaginationDirection, + val timestamp: Long, +) : TimelineItemVirtualModel { + override val type: String = "TimelineItemLoadingIndicatorModel" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemRoomBeginningModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemRoomBeginningModel.kt new file mode 100644 index 0000000000..8e2abad575 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemRoomBeginningModel.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 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.model.virtual + +data object TimelineItemRoomBeginningModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemRoomBeginningModel" +} 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 8df2c6d705..2774b71aef 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 @@ -31,6 +31,8 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory +import io.element.android.features.messages.impl.timeline.TimelineController +import io.element.android.features.messages.impl.timeline.TimelineItemIndexer import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider @@ -63,6 +65,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -81,6 +84,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender @@ -95,6 +99,9 @@ import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf @@ -167,7 +174,13 @@ class MessagesPresenterTest { @Test fun `present - handle toggling a reaction`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) - val room = FakeMatrixRoom() + val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) } + val toggleReactionFailure = lambdaRecorder { _: String, _: EventId -> Result.failure(IllegalStateException("Failed to send reaction")) } + + val timeline = FakeTimeline().apply { + this.toggleReactionLambda = toggleReactionSuccess + } + val room = FakeMatrixRoom(liveTimeline = timeline) val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -175,29 +188,42 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) - assertThat(room.myReactions.count()).isEqualTo(1) // No crashes when sending a reaction failed - room.givenToggleReactionResult(Result.failure(IllegalStateException("Failed to send reaction"))) + timeline.apply { toggleReactionLambda = toggleReactionFailure } initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) - assertThat(room.myReactions.count()).isEqualTo(1) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + + assert(toggleReactionSuccess) + .isCalledOnce() + .with(value("👍"), value(AN_EVENT_ID)) + assert(toggleReactionFailure) + .isCalledOnce() + .with(value("👍"), value(AN_EVENT_ID)) } } @Test fun `present - handle toggling a reaction twice`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) - val room = FakeMatrixRoom() + val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) } + + val timeline = FakeTimeline().apply { + this.toggleReactionLambda = toggleReactionSuccess + } + val room = FakeMatrixRoom(liveTimeline = timeline) val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) - assertThat(room.myReactions.count()).isEqualTo(1) - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) - assertThat(room.myReactions.count()).isEqualTo(0) + assert(toggleReactionSuccess) + .isCalledExactly(2) + .withSequence( + listOf(value("👍"), value(AN_EVENT_ID)), + listOf(value("👍"), value(AN_EVENT_ID)), + ) } } @@ -272,7 +298,7 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(3) + skipItems(2) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -428,7 +454,6 @@ class MessagesPresenterTest { initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent())) assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) - skipItems(1) // back paginating } } @@ -748,6 +773,7 @@ class MessagesPresenterTest { currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), permalinkParser = FakePermalinkParser(), permalinkBuilder = FakePermalinkBuilder(), + timelineController = TimelineController(matrixRoom), ) val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( this, @@ -768,6 +794,8 @@ class MessagesPresenterTest { endPollAction = endPollAction, sendPollResponseAction = FakeSendPollResponseAction(), sessionPreferencesStore = sessionPreferencesStore, + timelineItemIndexer = TimelineItemIndexer(), + timelineController = TimelineController(matrixRoom), ) val timelinePresenterFactory = object : TimelinePresenter.Factory { override fun create(navigator: MessagesNavigator): TimelinePresenter { @@ -804,6 +832,7 @@ class MessagesPresenterTest { buildMeta = aBuildMeta(), dispatchers = coroutineDispatchers, htmlConverterProvider = FakeHtmlConverterProvider(), + timelineController = TimelineController(matrixRoom), ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt index db056dea19..95dffe2bc0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.fixtures +import io.element.android.features.messages.impl.timeline.TimelineItemIndexer import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory @@ -46,7 +47,9 @@ import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorW import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope -internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { +internal fun TestScope.aTimelineItemsFactory( + timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer() +): TimelineItemsFactory { val timelineEventFormatter = aTimelineEventFormatter() val matrixClient = FakeMatrixClient() return TimelineItemsFactory( @@ -83,6 +86,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { ), ), timelineItemGrouper = TimelineItemGrouper(), + timelineItemIndexer = timelineItemIndexer, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTests.kt index c9acdba508..7715230511 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTests.kt @@ -21,14 +21,20 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import java.lang.IllegalStateException class ForwardMessagesPresenterTests { @get:Rule @@ -36,7 +42,7 @@ class ForwardMessagesPresenterTests { @Test fun `present - initial state`() = runTest { - val presenter = aPresenter() + val presenter = aForwardMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -49,7 +55,14 @@ class ForwardMessagesPresenterTests { @Test fun `present - forward successful`() = runTest { - val presenter = aPresenter() + val forwardEventLambda = lambdaRecorder { _: EventId, _: List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.forwardEventLambda = forwardEventLambda + } + val room = FakeMatrixRoom(liveTimeline = timeline) + val presenter = aForwardMessagesPresenter(fakeMatrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -61,18 +74,23 @@ class ForwardMessagesPresenterTests { val successfulForwardState = awaitItem() assertThat(successfulForwardState.isForwarding).isFalse() assertThat(successfulForwardState.forwardingSucceeded).isNotNull() + assert(forwardEventLambda).isCalledOnce() } } @Test fun `present - select a room and forward failed, then clear`() = runTest { - val room = FakeMatrixRoom() - val presenter = aPresenter(fakeMatrixRoom = room) + val forwardEventLambda = lambdaRecorder { _: EventId, _: List -> + Result.failure(IllegalStateException("error")) + } + val timeline = FakeTimeline().apply { + this.forwardEventLambda = forwardEventLambda + } + val room = FakeMatrixRoom(liveTimeline = timeline) + val presenter = aForwardMessagesPresenter(fakeMatrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - // Test failed forwarding - room.givenForwardEventResult(Result.failure(Throwable("error"))) skipItems(1) val summary = aRoomSummaryDetails() presenter.onRoomSelected(listOf(summary.roomId)) @@ -82,16 +100,17 @@ class ForwardMessagesPresenterTests { // Then clear error failedForwardState.eventSink(ForwardMessagesEvents.ClearError) assertThat(awaitItem().error).isNull() + assert(forwardEventLambda).isCalledOnce() } } - private fun CoroutineScope.aPresenter( + private fun CoroutineScope.aForwardMessagesPresenter( eventId: EventId = AN_EVENT_ID, fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(), coroutineScope: CoroutineScope = this, ) = ForwardMessagesPresenter( eventId = eventId.value, - room = fakeMatrixRoom, + timelineProvider = LiveTimelineProvider(fakeMatrixRoom), matrixCoroutineScope = coroutineScope, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index 290f297117..0e83aa84ec 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState +import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -65,6 +66,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor @@ -81,6 +83,10 @@ import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.any +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf @@ -259,7 +265,13 @@ class MessageComposerPresenterTest { @Test fun `present - edit sent message`() = runTest { - val fakeMatrixRoom = FakeMatrixRoom() + val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.editMessageLambda = editMessageLambda + } + val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline) val presenter = createPresenter( this, fakeMatrixRoom, @@ -283,7 +295,13 @@ class MessageComposerPresenterTest { skipItems(1) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) + + advanceUntilIdle() + + assert(editMessageLambda) + .isCalledOnce() + .with(any(), any(), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any()) + assertThat(analyticsService.capturedEvents).containsExactly( Composer( inThread = false, @@ -297,7 +315,13 @@ class MessageComposerPresenterTest { @Test fun `present - edit not sent message`() = runTest { - val fakeMatrixRoom = FakeMatrixRoom() + val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.editMessageLambda = editMessageLambda + } + val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline) val presenter = createPresenter( this, fakeMatrixRoom, @@ -321,7 +345,13 @@ class MessageComposerPresenterTest { skipItems(1) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) + + advanceUntilIdle() + + assert(editMessageLambda) + .isCalledOnce() + .with(any(), any(), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any()) + assertThat(analyticsService.capturedEvents).containsExactly( Composer( inThread = false, @@ -335,7 +365,13 @@ class MessageComposerPresenterTest { @Test fun `present - reply message`() = runTest { - val fakeMatrixRoom = FakeMatrixRoom() + val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.replyMessageLambda = replyMessageLambda + } + val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline) val presenter = createPresenter( this, fakeMatrixRoom, @@ -355,7 +391,13 @@ class MessageComposerPresenterTest { state.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage())) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY) + + advanceUntilIdle() + + assert(replyMessageLambda) + .isCalledOnce() + .with(any(), value(A_REPLY), value(A_REPLY), any()) + assertThat(analyticsService.capturedEvents).containsExactly( Composer( inThread = false, @@ -831,7 +873,17 @@ class MessageComposerPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - send messages with intentional mentions`() = runTest { - val room = FakeMatrixRoom() + val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List -> + Result.success(Unit) + } + val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.replyMessageLambda = replyMessageLambda + this.editMessageLambda = editMessageLambda + } + val room = FakeMatrixRoom(liveTimeline = timeline) val presenter = createPresenter(room = room, coroutineScope = this) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -866,7 +918,9 @@ class MessageComposerPresenterTest { initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) advanceUntilIdle() - assertThat(room.sendMessageMentions).isEqualTo(listOf(Mention.User(A_USER_ID_2))) + assert(replyMessageLambda) + .isCalledOnce() + .with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2)))) // Check intentional mentions on edit message skipItems(1) @@ -882,7 +936,9 @@ class MessageComposerPresenterTest { initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) advanceUntilIdle() - assertThat(room.sendMessageMentions).isEqualTo(listOf(Mention.User(A_USER_ID_3))) + assert(editMessageLambda) + .isCalledOnce() + .with(any(), any(), any(), any(), value(listOf(Mention.User(A_USER_ID_3)))) skipItems(1) } @@ -968,6 +1024,7 @@ class MessageComposerPresenterTest { permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter), permalinkParser = FakePermalinkParser(), permalinkBuilder = permalinkBuilder, + timelineController = TimelineController(room), ) private suspend fun ReceiveTurbine.awaitFirstItem(): T { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt new file mode 100644 index 0000000000..5bd1145aa3 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2024 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 + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.Mention +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_UNIQUE_ID +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class TimelineControllerTest { + @Test + fun `test switching between live and detached timeline`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val detachedTimeline = FakeTimeline(name = "detached") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + val sut = TimelineController(matrixRoom) + + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + assertThat(sut.isLive().first()).isTrue() + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline) + } + assertThat(sut.isLive().first()).isFalse() + assertThat(detachedTimeline.closeCounter).isEqualTo(0) + sut.focusOnLive() + assertThat(sut.isLive().first()).isTrue() + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + assertThat(detachedTimeline.closeCounter).isEqualTo(1) + } + } + + @Test + fun `test switching between detached 1 and detached 2 should close detached 1`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val detachedTimeline1 = FakeTimeline(name = "detached 1") + val detachedTimeline2 = FakeTimeline(name = "detached 2") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val sut = TimelineController(matrixRoom) + + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline1)) + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline1) + } + assertThat(detachedTimeline1.closeCounter).isEqualTo(0) + assertThat(detachedTimeline2.closeCounter).isEqualTo(0) + // Focus on another event should close the previous detached timeline + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline2)) + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline2) + } + assertThat(detachedTimeline1.closeCounter).isEqualTo(1) + assertThat(detachedTimeline2.closeCounter).isEqualTo(0) + } + } + + @Test + fun `test switching to live when already in live should have no effect`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val sut = TimelineController(matrixRoom) + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + assertThat(sut.isLive().first()).isTrue() + sut.focusOnLive() + assertThat(sut.isLive().first()).isTrue() + } + } + + @Test + fun `test closing the TimelineController should close the detached timeline`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val detachedTimeline = FakeTimeline(name = "detached") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + val sut = TimelineController(matrixRoom) + + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline) + } + assertThat(detachedTimeline.closeCounter).isEqualTo(0) + sut.close() + assertThat(detachedTimeline.closeCounter).isEqualTo(1) + } + } + + @Test + fun `test getting timeline item`() = runTest { + val liveTimeline = FakeTimeline( + name = "live", + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()) + ) + ) + ) + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val sut = TimelineController(matrixRoom) + assertThat(sut.timelineItems().first()).hasSize(1) + } + + @Test + fun `test invokeOnCurrentTimeline use the detached timeline and not the live timeline`() = runTest { + val lambdaForDetached = lambdaRecorder { _: String, _: String?, _: List -> + Result.success(Unit) + } + val lambdaForLive = lambdaRecorder(ensureNeverCalled = true) { _: String, _: String?, _: List -> + Result.success(Unit) + } + val liveTimeline = FakeTimeline(name = "live").apply { + sendMessageLambda = lambdaForLive + } + val detachedTimeline = FakeTimeline(name = "detached").apply { + sendMessageLambda = lambdaForDetached + } + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + val sut = TimelineController(matrixRoom) + sut.focusOnEvent(AN_EVENT_ID) + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline) + } + sut.invokeOnCurrentTimeline { + sendMessage("body", "htmlBody", emptyList()) + } + lambdaForDetached.assertions().isCalledOnce() + } + } + + @Test + fun `test last forward pagination on a detached timeline should switch to live timeline`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val detachedTimeline = FakeTimeline(name = "detached") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + val sut = TimelineController(matrixRoom) + + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline) + } + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> + Result.success(true) + } + detachedTimeline.apply { + this.paginateLambda = paginateLambda + } + sut.paginate(Timeline.PaginationDirection.FORWARDS) + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + } + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexerTest.kt new file mode 100644 index 0000000000..642f07a3f9 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexerTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 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 + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import org.junit.Test + +class TimelineItemIndexerTest { + @Test + fun `test TimelineItemIndexer`() { + val eventIds = mutableListOf() + val data = listOf( + aTimelineItemEvent().also { eventIds.add(it.eventId!!) }, + aTimelineItemEvent().also { eventIds.add(it.eventId!!) }, + aGroupedEvents().also { groupedEvents -> + groupedEvents.events.forEach { eventIds.add(it.eventId!!) } + }, + TimelineItem.Virtual( + id = "dummy", + model = TimelineItemReadMarkerModel + ), + ) + assertThat(eventIds.size).isEqualTo(4) + val sut = TimelineItemIndexer() + sut.process(data) + eventIds.forEach { + assertThat(sut.isKnown(it)).isTrue() + } + assertThat(sut.indexOf(eventIds[0])).isEqualTo(0) + assertThat(sut.indexOf(eventIds[1])).isEqualTo(1) + assertThat(sut.indexOf(eventIds[2])).isEqualTo(2) + assertThat(sut.indexOf(eventIds[3])).isEqualTo(2) + + // Unknown event + assertThat(sut.isKnown(AN_EVENT_ID)).isFalse() + assertThat(sut.indexOf(AN_EVENT_ID)).isEqualTo(-1) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 2485e57172..093306db09 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -22,6 +22,7 @@ import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.FakeMessagesNavigator +import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.NewEventState @@ -33,12 +34,12 @@ import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem 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.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.Receipt @@ -48,20 +49,27 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember -import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitLastSequentialItem -import io.element.android.tests.testutils.awaitWithLatch import io.element.android.tests.testutils.consumeItemsUntilPredicate +import io.element.android.tests.testutils.lambda.any +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -72,7 +80,7 @@ import kotlin.time.Duration.Companion.seconds private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID" private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" -class TimelinePresenterTest { +@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -84,58 +92,49 @@ class TimelinePresenterTest { }.test { val initialState = awaitFirstItem() assertThat(initialState.timelineItems).isEmpty() - val loadedNoTimelineState = awaitItem() - assertThat(loadedNoTimelineState.timelineItems).isEmpty() + assertThat(initialState.isLive).isTrue() + assertThat(initialState.newEventState).isEqualTo(NewEventState.None) + assertThat(initialState.focusedEventId).isNull() + assertThat(initialState.focusRequestState).isEqualTo(FocusRequestState.None) } } @Test fun `present - load more`() = runTest { - val presenter = createTimelinePresenter() + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> + Result.success(false) + } + val timeline = FakeTimeline().apply { + this.paginateLambda = paginateLambda + } + val presenter = createTimelinePresenter(timeline = timeline) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.paginationState.hasMoreToLoadBackwards).isTrue() - assertThat(initialState.paginationState.isBackPaginating).isFalse() - initialState.eventSink.invoke(TimelineEvents.LoadMore) - val inPaginationState = awaitItem() - assertThat(inPaginationState.paginationState.isBackPaginating).isTrue() - assertThat(inPaginationState.paginationState.hasMoreToLoadBackwards).isTrue() - val postPaginationState = awaitItem() - assertThat(postPaginationState.paginationState.hasMoreToLoadBackwards).isTrue() - assertThat(postPaginationState.paginationState.isBackPaginating).isFalse() - } - } - - @Test - fun `present - set highlighted event`() = runTest { - val presenter = createTimelinePresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitFirstItem() - skipItems(1) - assertThat(initialState.highlightedEventId).isNull() - initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(AN_EVENT_ID)) - val withHighlightedState = awaitItem() - assertThat(withHighlightedState.highlightedEventId).isEqualTo(AN_EVENT_ID) - initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(null)) - val withoutHighlightedState = awaitItem() - assertThat(withoutHighlightedState.highlightedEventId).isNull() + initialState.eventSink.invoke(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)) + initialState.eventSink.invoke(TimelineEvents.LoadMore(Timeline.PaginationDirection.FORWARDS)) + assert(paginateLambda) + .isCalledExactly(2) + .withSequence( + listOf(value(Timeline.PaginationDirection.BACKWARDS)), + listOf(value(Timeline.PaginationDirection.FORWARDS)) + ) } } @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) { - val timeline = FakeMatrixTimeline( - initialTimelineItems = listOf( - MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()) + val timeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()) + ) ) ) + val room = FakeMatrixRoom(liveTimeline = timeline) val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false) - val room = FakeMatrixRoom(matrixTimeline = timeline) val presenter = createTimelinePresenter( timeline = timeline, room = room, @@ -144,7 +143,6 @@ class TimelinePresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sentReadReceipts).isEmpty() val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) runCurrent() @@ -155,48 +153,62 @@ class TimelinePresenterTest { @Test fun `present - on scroll finished send read receipt if an event is before the index`() = runTest { - val timeline = FakeMatrixTimeline( - initialTimelineItems = listOf( - MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), - MatrixTimelineItem.Event( - uniqueId = FAKE_UNIQUE_ID_2, - event = anEventTimelineItem( - eventId = AN_EVENT_ID_2, - content = aMessageContent("Test message") + val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType -> + Result.success(Unit) + } + val timeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), + MatrixTimelineItem.Event( + uniqueId = FAKE_UNIQUE_ID_2, + event = anEventTimelineItem( + eventId = AN_EVENT_ID_2, + content = aMessageContent("Test message") + ) ) ) ) - ) + ).apply { + this.sendReadReceiptLambda = sendReadReceiptsLambda + } val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sentReadReceipts).isEmpty() - val initialState = awaitFirstItem() - awaitWithLatch { latch -> - timeline.sendReadReceiptLatch = latch - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) + skipItems(1) + awaitItem().run { + eventSink.invoke(TimelineEvents.OnScrollFinished(1)) } - assertThat(timeline.sentReadReceipts).isNotEmpty() - assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ) + advanceUntilIdle() + assert(sendReadReceiptsLambda) + .isCalledOnce() + .with(any(), value(ReceiptType.READ)) cancelAndIgnoreRemainingEvents() } } @Test fun `present - on scroll finished send a private read receipt if an event is at an index other than 0 and public read receipts are disabled`() = runTest { - val timeline = FakeMatrixTimeline( - initialTimelineItems = listOf( - MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), - MatrixTimelineItem.Event( - uniqueId = FAKE_UNIQUE_ID_2, - event = anEventTimelineItem( - eventId = AN_EVENT_ID_2, - content = aMessageContent("Test message") + val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType -> + Result.success(Unit) + } + val timeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), + MatrixTimelineItem.Event( + uniqueId = FAKE_UNIQUE_ID_2, + event = anEventTimelineItem( + eventId = AN_EVENT_ID_2, + content = aMessageContent("Test message") + ) ) ) ) - ) + ).apply { + this.sendReadReceiptLambda = sendReadReceiptsLambda + } val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false) val presenter = createTimelinePresenter( timeline = timeline, @@ -205,75 +217,86 @@ class TimelinePresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sentReadReceipts).isEmpty() - val initialState = awaitFirstItem() - awaitWithLatch { latch -> - timeline.sendReadReceiptLatch = latch - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) + skipItems(1) + awaitItem().run { + eventSink.invoke(TimelineEvents.OnScrollFinished(0)) + eventSink.invoke(TimelineEvents.OnScrollFinished(1)) } - assertThat(timeline.sentReadReceipts).isNotEmpty() - assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE) + advanceUntilIdle() + assert(sendReadReceiptsLambda) + .isCalledOnce() + .with(any(), value(ReceiptType.READ_PRIVATE)) cancelAndIgnoreRemainingEvents() } } @Test fun `present - on scroll finished will not send read receipt the first visible event is the same as before`() = runTest { - val timeline = FakeMatrixTimeline( - initialTimelineItems = listOf( - MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), - MatrixTimelineItem.Event( - uniqueId = FAKE_UNIQUE_ID_2, - event = anEventTimelineItem( - eventId = AN_EVENT_ID_2, - content = aMessageContent("Test message") + val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType -> + Result.success(Unit) + } + val timeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), + MatrixTimelineItem.Event( + uniqueId = FAKE_UNIQUE_ID_2, + event = anEventTimelineItem( + eventId = AN_EVENT_ID_2, + content = aMessageContent("Test message") + ) ) ) ) - ) + ).apply { + this.sendReadReceiptLambda = sendReadReceiptsLambda + } val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sentReadReceipts).isEmpty() - val initialState = awaitFirstItem() - awaitWithLatch { latch -> - timeline.sendReadReceiptLatch = latch - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) + skipItems(1) + awaitItem().run { + eventSink.invoke(TimelineEvents.OnScrollFinished(1)) + eventSink.invoke(TimelineEvents.OnScrollFinished(1)) } - assertThat(timeline.sentReadReceipts).hasSize(1) + advanceUntilIdle() cancelAndIgnoreRemainingEvents() + assert(sendReadReceiptsLambda).isCalledOnce() } } @Test fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest { - val timeline = FakeMatrixTimeline( - initialTimelineItems = listOf( - MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker), - MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker) + val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType -> + Result.success(Unit) + } + val timeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker), + MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker) + ) ) - ) + ).apply { + this.sendReadReceiptLambda = sendReadReceiptsLambda + } val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sentReadReceipts).isEmpty() + skipItems(1) val initialState = awaitFirstItem() - awaitWithLatch { latch -> - timeline.sendReadReceiptLatch = latch - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) - } - assertThat(timeline.sentReadReceipts).isEmpty() + initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) cancelAndIgnoreRemainingEvents() + assert(sendReadReceiptsLambda).isNeverCalled() } } @Test fun `present - covers newEventState scenarios`() = runTest { - val timeline = FakeMatrixTimeline() + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -281,12 +304,12 @@ class TimelinePresenterTest { val initialState = awaitFirstItem() assertThat(initialState.newEventState).isEqualTo(NewEventState.None) assertThat(initialState.timelineItems.size).isEqualTo(0) - timeline.updateTimelineItems { + timelineItems.emit( listOf(MatrixTimelineItem.Event("0", anEventTimelineItem(content = aMessageContent()))) - } + ) consumeItemsUntilPredicate { it.timelineItems.size == 1 } // Mimics sending a message, and assert newEventState is FromMe - timeline.updateTimelineItems { items -> + timelineItems.getAndUpdate { items -> val event = anEventTimelineItem(content = aMessageContent(), isOwn = true) items + listOf(MatrixTimelineItem.Event("1", event)) } @@ -295,7 +318,7 @@ class TimelinePresenterTest { assertThat(state.newEventState).isEqualTo(NewEventState.FromMe) } // Mimics receiving a message without clearing the previous FromMe - timeline.updateTimelineItems { items -> + timelineItems.getAndUpdate { items -> val event = anEventTimelineItem(content = aMessageContent()) items + listOf(MatrixTimelineItem.Event("2", event)) } @@ -307,7 +330,7 @@ class TimelinePresenterTest { assertThat(state.newEventState).isEqualTo(NewEventState.None) } // Mimics receiving a message and assert newEventState is FromOther - timeline.updateTimelineItems { items -> + timelineItems.getAndUpdate { items -> val event = anEventTimelineItem(content = aMessageContent()) items + listOf(MatrixTimelineItem.Event("3", event)) } @@ -321,7 +344,10 @@ class TimelinePresenterTest { @Test fun `present - reaction ordering`() = runTest { - val timeline = FakeMatrixTimeline() + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline( + timelineItems = timelineItems, + ) val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -349,10 +375,9 @@ class TimelinePresenterTest { senders = persistentListOf(charlie) ), ) - timeline.updateTimelineItems { + timelineItems.emit( listOf(MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction))) - } - skipItems(1) + ) val item = awaitItem().timelineItems.first() assertThat(item).isInstanceOf(TimelineItem.Event::class.java) val event = item as TimelineItem.Event @@ -424,8 +449,10 @@ class TimelinePresenterTest { fun `present - side effect on redacted items is invoked`() = runTest { val redactedVoiceMessageManager = FakeRedactedVoiceMessageManager() val presenter = createTimelinePresenter( - timeline = FakeMatrixTimeline( - initialTimelineItems = aRedactedMatrixTimeline(AN_EVENT_ID), + timeline = FakeTimeline( + timelineItems = flowOf( + aRedactedMatrixTimeline(AN_EVENT_ID), + ) ), redactedVoiceMessageManager = redactedVoiceMessageManager, ) @@ -433,32 +460,141 @@ class TimelinePresenterTest { presenter.present() }.test { assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0) - awaitFirstItem().let { - assertThat(it.timelineItems).isNotEmpty() - } + skipItems(2) assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1) } } + @Test + fun `present - focus on event and jump to live make the presenter update the state with the correct Events`() = runTest { + val detachedTimeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = FAKE_UNIQUE_ID, + event = anEventTimelineItem(), + ) + ) + ) + ) + val liveTimeline = FakeTimeline( + timelineItems = flowOf(emptyList()) + ) + val room = FakeMatrixRoom( + liveTimeline = liveTimeline, + ).apply { + givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + } + val presenter = createTimelinePresenter( + room = room, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Fetching) + } + skipItems(2) + awaitItem().also { state -> + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Fetched) + assertThat(state.timelineItems).isNotEmpty() + } + initialState.eventSink.invoke(TimelineEvents.JumpToLive) + skipItems(1) + awaitItem().also { state -> + // Event stays focused + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.timelineItems).isEmpty() + } + } + } + + @Test + fun `present - focus on known event retrieves the event from cache`() = runTest { + val timelineItemIndexer = TimelineItemIndexer().apply { + process(listOf(aMessageEvent(eventId = AN_EVENT_ID))) + } + val presenter = createTimelinePresenter( + room = FakeMatrixRoom( + liveTimeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = FAKE_UNIQUE_ID, + event = anEventTimelineItem(eventId = AN_EVENT_ID), + ) + ) + ) + ), + ), + timelineItemIndexer = timelineItemIndexer, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Cached(0)) + } + } + } + + @Test + fun `present - focus on event error case`() = runTest { + val presenter = createTimelinePresenter( + room = FakeMatrixRoom( + liveTimeline = FakeTimeline( + timelineItems = flowOf(emptyList()), + ), + ).apply { + givenTimelineFocusedOnEventResult(Result.failure(Throwable("An error"))) + }, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Fetching) + } + awaitItem().also { state -> + assertThat(state.focusRequestState).isInstanceOf(FocusRequestState.Failure::class.java) + state.eventSink(TimelineEvents.ClearFocusRequestState) + } + awaitItem().also { state -> + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.None) + } + } + } + @Test fun `present - when room member info is loaded, read receipts info should be updated`() = runTest { - val timeline = FakeMatrixTimeline( - listOf( - MatrixTimelineItem.Event( - FAKE_UNIQUE_ID, - anEventTimelineItem( - sender = A_USER_ID, - receipts = persistentListOf( - Receipt( - userId = A_USER_ID, - timestamp = 0L, + val timeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + FAKE_UNIQUE_ID, + anEventTimelineItem( + sender = A_USER_ID, + receipts = persistentListOf( + Receipt( + userId = A_USER_ID, + timestamp = 0L, + ) ) ) ) ) ) ) - val room = FakeMatrixRoom(matrixTimeline = timeline).apply { + val room = FakeMatrixRoom(liveTimeline = timeline).apply { givenRoomMembersState(MatrixRoomMembersState.Unknown) } @@ -485,22 +621,19 @@ class TimelinePresenterTest { } private suspend fun ReceiveTurbine.awaitFirstItem(): T { - // Skip 1 item if Mentions feature is enabled - if (FeatureFlags.Mentions.defaultValue) { - skipItems(1) - } return awaitItem() } private fun TestScope.createTimelinePresenter( - timeline: MatrixTimeline = FakeMatrixTimeline(), - room: FakeMatrixRoom = FakeMatrixRoom(matrixTimeline = timeline), + timeline: Timeline = FakeTimeline(), + room: FakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline), timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), endPollAction: EndPollAction = FakeEndPollAction(), sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), + timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactory = timelineItemsFactory, @@ -512,6 +645,8 @@ class TimelinePresenterTest { endPollAction = endPollAction, sendPollResponseAction = sendPollResponseAction, sessionPreferencesStore = sessionPreferencesStore, + timelineItemIndexer = timelineItemIndexer, + timelineController = TimelineController(room), ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 44fb6270ae..29d46d2da2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -17,14 +17,25 @@ package io.element.android.features.messages.impl.timeline import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel +import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.aTypingNotificationState +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EventsRecorder +import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @@ -34,55 +45,89 @@ class TimelineViewTest { @Test fun `reaching the end of the timeline with more events to load emits a LoadMore event`() { val eventsRecorder = EventsRecorder() - rule.setContent { - TimelineView( - aTimelineState( - eventSink = eventsRecorder, - paginationState = aPaginationState( - hasMoreToLoadBackwards = true, - ) + rule.setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf( + TimelineItem.Virtual( + id = "backward_pagination", + model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0) + ), ), - typingNotificationState = aTypingNotificationState(), - roomName = null, - onUserDataClicked = EnsureNeverCalledWithParam(), - onLinkClicked = EnsureNeverCalledWithParam(), - onMessageClicked = EnsureNeverCalledWithParam(), - onMessageLongClicked = EnsureNeverCalledWithParam(), - onTimestampClicked = EnsureNeverCalledWithParam(), - onSwipeToReply = EnsureNeverCalledWithParam(), - onReactionClicked = EnsureNeverCalledWithTwoParams(), - onReactionLongClicked = EnsureNeverCalledWithTwoParams(), - onMoreReactionsClicked = EnsureNeverCalledWithParam(), - onReadReceiptClick = EnsureNeverCalledWithParam(), - ) - } - eventsRecorder.assertSingle(TimelineEvents.LoadMore) + eventSink = eventsRecorder, + ), + ) + eventsRecorder.assertSingle(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)) } @Test fun `reaching the end of the timeline does not send a LoadMore event`() { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setContent { - TimelineView( - aTimelineState( - eventSink = eventsRecorder, - paginationState = aPaginationState( - hasMoreToLoadBackwards = false, - ) - ), - typingNotificationState = aTypingNotificationState(), - roomName = null, - onUserDataClicked = EnsureNeverCalledWithParam(), - onLinkClicked = EnsureNeverCalledWithParam(), - onMessageClicked = EnsureNeverCalledWithParam(), - onMessageLongClicked = EnsureNeverCalledWithParam(), - onTimestampClicked = EnsureNeverCalledWithParam(), - onSwipeToReply = EnsureNeverCalledWithParam(), - onReactionClicked = EnsureNeverCalledWithTwoParams(), - onReactionLongClicked = EnsureNeverCalledWithTwoParams(), - onMoreReactionsClicked = EnsureNeverCalledWithParam(), - onReadReceiptClick = EnsureNeverCalledWithParam(), - ) - } + rule.setTimelineView( + state = aTimelineState( + eventSink = eventsRecorder, + ), + ) + } + + @Test + fun `scroll to bottom on live timeline does not emit the Event`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setTimelineView( + state = aTimelineState( + isLive = true, + eventSink = eventsRecorder, + ), + forceJumpToBottomVisibility = true, + ) + val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) + rule.onNodeWithContentDescription(contentDescription).performClick() + } + + @Test + fun `scroll to bottom on detached timeline emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setTimelineView( + state = aTimelineState( + isLive = false, + eventSink = eventsRecorder, + ), + ) + val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) + rule.onNodeWithContentDescription(contentDescription).performClick() + eventsRecorder.assertSingle(TimelineEvents.JumpToLive) + } +} + +private fun AndroidComposeTestRule.setTimelineView( + state: TimelineState, + typingNotificationState: TypingNotificationState = aTypingNotificationState(), + onUserDataClicked: (UserId) -> Unit = EnsureNeverCalledWithParam(), + onLinkClicked: (String) -> Unit = EnsureNeverCalledWithParam(), + onMessageClicked: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onMessageLongClicked: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onTimestampClicked: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onSwipeToReply: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(), + onReactionLongClicked: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(), + onMoreReactionsClicked: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + forceJumpToBottomVisibility: Boolean = false, +) { + setContent { + TimelineView( + state = state, + typingNotificationState = typingNotificationState, + onUserDataClicked = onUserDataClicked, + onLinkClicked = onLinkClicked, + onMessageClicked = onMessageClicked, + onMessageLongClicked = onMessageLongClicked, + onTimestampClicked = onTimestampClicked, + onSwipeToReply = onSwipeToReply, + onReactionClicked = onReactionClicked, + onReactionLongClicked = onReactionLongClicked, + onMoreReactionsClicked = onMoreReactionsClicked, + onReadReceiptClick = onReadReceiptClick, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, + ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt index 86355f82dc..d73f7246c9 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt @@ -32,22 +32,22 @@ import org.junit.Test class InReplyToDetailTest { @Test - fun `map - with a not ready InReplyTo does not work`() { + fun `map - with a not ready InReplyTo return expected object`() { assertThat( - InReplyTo.Pending.map( + InReplyTo.Pending(AN_EVENT_ID).map( permalinkParser = FakePermalinkParser() ) - ).isNull() + ).isEqualTo(InReplyToDetails.Loading(AN_EVENT_ID)) assertThat( InReplyTo.NotLoaded(AN_EVENT_ID).map( permalinkParser = FakePermalinkParser() ) - ).isNull() + ).isEqualTo(InReplyToDetails.Loading(AN_EVENT_ID)) assertThat( - InReplyTo.Error.map( + InReplyTo.Error(AN_EVENT_ID, "a message").map( permalinkParser = FakePermalinkParser() ) - ).isNull() + ).isEqualTo(InReplyToDetails.Error(AN_EVENT_ID, "a message")) } @Test @@ -65,7 +65,7 @@ class InReplyToDetailTest { permalinkParser = FakePermalinkParser() ) assertThat(inReplyToDetails).isNotNull() - assertThat(inReplyToDetails?.textContent).isNull() + assertThat((inReplyToDetails as InReplyToDetails.Ready).textContent).isNull() } @Test @@ -89,9 +89,7 @@ class InReplyToDetailTest { ) ) assertThat( - inReplyTo.map( - permalinkParser = FakePermalinkParser() - )?.textContent + (inReplyTo.map(permalinkParser = FakePermalinkParser()) as InReplyToDetails.Ready).textContent ).isEqualTo("Hello!") } @@ -113,9 +111,7 @@ class InReplyToDetailTest { ) ) assertThat( - inReplyTo.map( - permalinkParser = FakePermalinkParser() - )?.textContent + (inReplyTo.map(permalinkParser = FakePermalinkParser()) as InReplyToDetails.Ready).textContent ).isEqualTo("**Hello!**") } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadataKtTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadataKtTest.kt index dc91de52b0..b56d871704 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadataKtTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadataKtTest.kt @@ -70,7 +70,7 @@ class InReplyToMetadataKtTest { @Test fun `any message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails(eventContent = aMessageContent()).metadata() + anInReplyToDetailsReady(eventContent = aMessageContent()).metadata() }.test { awaitItem().let { assertThat(it).isEqualTo(InReplyToMetadata.Text("textContent")) @@ -81,7 +81,7 @@ class InReplyToMetadataKtTest { @Test fun `an image message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = aMessageContent( messageType = ImageMessageType( body = "body", @@ -111,7 +111,7 @@ class InReplyToMetadataKtTest { @Test fun `a sticker message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = StickerContent( body = "body", info = anImageInfo(), @@ -137,7 +137,7 @@ class InReplyToMetadataKtTest { @Test fun `a video message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = aMessageContent( messageType = VideoMessageType( body = "body", @@ -167,7 +167,7 @@ class InReplyToMetadataKtTest { @Test fun `a file message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = aMessageContent( messageType = FileMessageType( body = "body", @@ -200,7 +200,7 @@ class InReplyToMetadataKtTest { @Test fun `a audio message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = aMessageContent( messageType = AudioMessageType( body = "body", @@ -232,7 +232,7 @@ class InReplyToMetadataKtTest { fun `a location message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { testEnv { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = aMessageContent( messageType = LocationMessageType( body = "body", @@ -262,7 +262,7 @@ class InReplyToMetadataKtTest { fun `a voice message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { testEnv { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = aMessageContent( messageType = VoiceMessageType( body = "body", @@ -292,7 +292,7 @@ class InReplyToMetadataKtTest { @Test fun `a poll content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = aPollContent() ).metadata() }.test { @@ -314,7 +314,7 @@ class InReplyToMetadataKtTest { @Test fun `redacted content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = RedactedContent ).metadata() }.test { @@ -327,7 +327,7 @@ class InReplyToMetadataKtTest { @Test fun `unable to decrypt content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = UnableToDecryptContent(UnableToDecryptContent.Data.Unknown) ).metadata() }.test { @@ -340,7 +340,7 @@ class InReplyToMetadataKtTest { @Test fun `failed to parse message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = FailedToParseMessageLikeContent("", "") ).metadata() }.test { @@ -353,7 +353,7 @@ class InReplyToMetadataKtTest { @Test fun `failed to parse state content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = FailedToParseStateContent("", "", "") ).metadata() }.test { @@ -366,7 +366,7 @@ class InReplyToMetadataKtTest { @Test fun `profile change content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = ProfileChangeContent("", "", "", "") ).metadata() }.test { @@ -379,7 +379,7 @@ class InReplyToMetadataKtTest { @Test fun `room membership content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = RoomMembershipContent(A_USER_ID, null) ).metadata() }.test { @@ -392,7 +392,7 @@ class InReplyToMetadataKtTest { @Test fun `state content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = StateContent("", OtherState.RoomJoinRules) ).metadata() }.test { @@ -405,7 +405,7 @@ class InReplyToMetadataKtTest { @Test fun `unknown content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = UnknownContent ).metadata() }.test { @@ -418,7 +418,7 @@ class InReplyToMetadataKtTest { @Test fun `null content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetails( + anInReplyToDetailsReady( eventContent = null ).metadata() }.test { @@ -429,13 +429,13 @@ class InReplyToMetadataKtTest { } } -fun anInReplyToDetails( +private fun anInReplyToDetailsReady( eventId: EventId = AN_EVENT_ID, senderId: UserId = A_USER_ID, senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(), eventContent: EventContent? = aMessageContent(), textContent: String? = "textContent", -) = InReplyToDetails( +) = InReplyToDetails.Ready( eventId = eventId, senderId = senderId, senderProfile = senderProfile, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt index 0b9aa5aee0..e8d2506408 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt @@ -20,15 +20,19 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.TimelineProvider +import io.element.android.libraries.matrix.api.timeline.getActiveTimeline import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import kotlinx.coroutines.flow.first import javax.inject.Inject class PollRepository @Inject constructor( private val room: MatrixRoom, + private val timelineProvider: TimelineProvider, ) { suspend fun getPoll(eventId: EventId): Result = runCatching { - room.timeline + timelineProvider + .getActiveTimeline() .timelineItems .first() .asSequence() @@ -51,13 +55,15 @@ class PollRepository @Inject constructor( maxSelections = maxSelections, pollKind = pollKind, ) - else -> room.editPoll( - pollStartId = existingPollId, - question = question, - answers = answers, - maxSelections = maxSelections, - pollKind = pollKind, - ) + else -> timelineProvider + .getActiveTimeline() + .editPoll( + pollStartId = existingPollId, + question = question, + answers = answers, + maxSelections = maxSelections, + pollKind = pollKind, + ) } suspend fun deletePoll( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt index da37891d91..981fe00c32 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt @@ -32,25 +32,24 @@ import io.element.android.features.poll.impl.history.model.PollHistoryFilter import io.element.android.features.poll.impl.history.model.PollHistoryItems import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.TimelineProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject class PollHistoryPresenter @Inject constructor( - private val room: MatrixRoom, private val appCoroutineScope: CoroutineScope, private val sendPollResponseAction: SendPollResponseAction, private val endPollAction: EndPollAction, private val pollHistoryItemFactory: PollHistoryItemsFactory, + private val timelineProvider: TimelineProvider, ) : Presenter { @Composable override fun present(): PollHistoryState { - // TODO use room.rememberPollHistory() when working properly? - val timeline = room.timeline - val paginationState by timeline.paginationState.collectAsState() + val timeline by timelineProvider.activeTimelineFlow().collectAsState() + val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState() val pollHistoryItemsFlow = remember { timeline.timelineItems.map { items -> pollHistoryItemFactory.create(items) @@ -61,11 +60,11 @@ class PollHistoryPresenter @Inject constructor( } val pollHistoryItems by pollHistoryItemsFlow.collectAsState(initial = PollHistoryItems()) LaunchedEffect(paginationState, pollHistoryItems.size) { - if (pollHistoryItems.size == 0 && paginationState.canBackPaginate) loadMore(timeline) + if (pollHistoryItems.size == 0 && paginationState.canPaginate) loadMore(timeline) } val isLoading by remember { derivedStateOf { - pollHistoryItems.size == 0 || paginationState.isBackPaginating + pollHistoryItems.size == 0 || paginationState.isPaginating } } val coroutineScope = rememberCoroutineScope() @@ -88,14 +87,14 @@ class PollHistoryPresenter @Inject constructor( return PollHistoryState( isLoading = isLoading, - hasMoreToLoad = paginationState.hasMoreToLoadBackwards, + hasMoreToLoad = paginationState.hasMoreToLoad, pollHistoryItems = pollHistoryItems, activeFilter = activeFilter, eventSink = ::handleEvents, ) } - private fun CoroutineScope.loadMore(pollHistory: MatrixTimeline) = launch { - pollHistory.paginateBackwards(200) + private fun CoroutineScope.loadMore(pollHistory: Timeline) = launch { + pollHistory.paginate(Timeline.PaginationDirection.BACKWARDS) } } diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt index b54b18b8c0..c0a5b4f7f7 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt @@ -20,16 +20,17 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.PollContent -import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf -fun aPollTimeline( +fun aPollTimelineItems( polls: Map = emptyMap(), -): FakeMatrixTimeline { - return FakeMatrixTimeline( - initialTimelineItems = polls.map { entry -> +): Flow> { + return flowOf( + polls.map { entry -> MatrixTimelineItem.Event( entry.key.value, anEventTimelineItem( diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index d5be6c2bb6..2dbc612a0b 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -25,33 +25,42 @@ import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.PollCreation import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.poll.api.create.CreatePollMode -import io.element.android.features.poll.impl.aPollTimeline +import io.element.android.features.poll.impl.aPollTimelineItems import io.element.android.features.poll.impl.anOngoingPollContent import io.element.android.features.poll.impl.data.PollRepository +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.SavePollInvocation +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -class CreatePollPresenterTest { +@OptIn(ExperimentalCoroutinesApi::class) class CreatePollPresenterTest { @get:Rule val warmUpRule = WarmUpRule() private val pollEventId = AN_EVENT_ID private var navUpInvocationsCount = 0 private val existingPoll = anOngoingPollContent() + private val timeline = FakeTimeline( + timelineItems = aPollTimelineItems(mapOf(pollEventId to existingPoll)) + ) private val fakeMatrixRoom = FakeMatrixRoom( - matrixTimeline = aPollTimeline( - mapOf(pollEventId to existingPoll) - ) + liveTimeline = timeline ) private val fakeAnalyticsService = FakeAnalyticsService() private val fakeMessageComposerContext = FakeMessageComposerContext() @@ -80,7 +89,7 @@ class CreatePollPresenterTest { @Test fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest { val room = FakeMatrixRoom( - matrixTimeline = aPollTimeline() + liveTimeline = FakeTimeline() ) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -180,6 +189,12 @@ class CreatePollPresenterTest { @Test fun `edit poll sends a poll edit event`() = runTest { + val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List, _: Int, _: PollKind -> + Result.success(Unit) + } + timeline.apply { + this.editPollLambda = editPollLambda + } val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -201,16 +216,18 @@ class CreatePollPresenterTest { ).apply { eventSink(CreatePollEvents.Save) } - delay(1) // Wait for the coroutine to finish - assertThat(fakeMatrixRoom.editPollInvocations.size).isEqualTo(1) - assertThat(fakeMatrixRoom.editPollInvocations.last()).isEqualTo( - SavePollInvocation( - question = "Changed question", - answers = listOf("Changed answer 1", "Changed answer 2", "Maybe"), - maxSelections = 1, - pollKind = PollKind.Disclosed + advanceUntilIdle() // Wait for the coroutine to finish + + assert(editPollLambda) + .isCalledOnce() + .with( + value(pollEventId), + value("Changed question"), + value(listOf("Changed answer 1", "Changed answer 2", "Maybe")), + value(1), + value(PollKind.Disclosed) ) - ) + assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2) assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo( Composer( @@ -233,6 +250,12 @@ class CreatePollPresenterTest { @Test fun `when edit poll fails, error is tracked`() = runTest { val error = Exception("cause") + val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List, _: Int, _: PollKind -> + Result.failure(error) + } + timeline.apply { + this.editPollLambda = editPollLambda + } fakeMatrixRoom.givenEditPollResult(Result.failure(error)) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) moleculeFlow(RecompositionMode.Immediate) { @@ -241,8 +264,8 @@ class CreatePollPresenterTest { awaitDefaultItem() awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A")) awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save) - delay(1) // Wait for the coroutine to finish - assertThat(fakeMatrixRoom.editPollInvocations).hasSize(1) + advanceUntilIdle() // Wait for the coroutine to finish + assert(editPollLambda).isCalledOnce() assertThat(fakeAnalyticsService.capturedEvents).isEmpty() assertThat(fakeAnalyticsService.trackedErrors).hasSize(1) assertThat(fakeAnalyticsService.trackedErrors).containsExactly( @@ -497,22 +520,22 @@ class CreatePollPresenterTest { newAnswer1: String? = null, newAnswer2: String? = null, ) = - awaitItem().apply { - assertThat(canSave).isTrue() - assertThat(canAddAnswer).isTrue() - assertThat(question).isEqualTo(newQuestion ?: existingPoll.question) - assertThat(answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply { + awaitItem().also { state -> + assertThat(state.canSave).isTrue() + assertThat(state.canAddAnswer).isTrue() + assertThat(state.question).isEqualTo(newQuestion ?: existingPoll.question) + assertThat(state.answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply { newAnswer1?.let { this[0] = Answer(it, true) } newAnswer2?.let { this[1] = Answer(it, true) } }) - assertThat(pollKind).isEqualTo(existingPoll.kind) + assertThat(state.pollKind).isEqualTo(existingPoll.kind) } private fun createCreatePollPresenter( mode: CreatePollMode = CreatePollMode.NewPoll, room: MatrixRoom = fakeMatrixRoom, ): CreatePollPresenter = CreatePollPresenter( - repository = PollRepository(room), + repository = PollRepository(room, LiveTimelineProvider(room)), analyticsService = fakeAnalyticsService, messageComposerContext = fakeMessageComposerContext, navigateUp = { navUpInvocationsCount++ }, diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt index f7948a951c..638f9e6ff0 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt @@ -22,7 +22,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction -import io.element.android.features.poll.impl.aPollTimeline +import io.element.android.features.poll.impl.aPollTimelineItems import io.element.android.features.poll.impl.anEndedPollContent import io.element.android.features.poll.impl.anOngoingPollContent import io.element.android.features.poll.impl.history.model.PollHistoryFilter @@ -32,14 +32,21 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -50,14 +57,18 @@ class PollHistoryPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val timeline = aPollTimeline( - polls = mapOf( - AN_EVENT_ID to anOngoingPollContent(), - AN_EVENT_ID_2 to anEndedPollContent() - ) + private val backwardPaginationStatus = MutableStateFlow(Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)) + private val timeline = FakeTimeline( + timelineItems = aPollTimelineItems( + mapOf( + AN_EVENT_ID to anOngoingPollContent(), + AN_EVENT_ID_2 to anEndedPollContent() + ) + ), + backwardPaginationStatus = backwardPaginationStatus ) private val room = FakeMatrixRoom( - matrixTimeline = timeline + liveTimeline = timeline ) @Test @@ -66,7 +77,6 @@ class PollHistoryPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) awaitItem().also { state -> assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING) assertThat(state.pollHistoryItems.size).isEqualTo(0) @@ -127,26 +137,30 @@ class PollHistoryPresenterTest { @Test fun `present - load more scenario`() = runTest { + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> + Result.success(false) + } + timeline.apply { + this.paginateLambda = paginateLambda + } val presenter = createPollHistoryPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.pollHistoryItems.size).isEqualTo(2) - } - timeline.updatePaginationState { - copy(isBackPaginating = false) - } + skipItems(1) val loadedState = awaitItem() assertThat(loadedState.isLoading).isFalse() loadedState.eventSink(PollHistoryEvents.LoadMore) + backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = true) } awaitItem().also { state -> assertThat(state.isLoading).isTrue() } + backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = false) } awaitItem().also { state -> assertThat(state.isLoading).isFalse() } + // Called once by the initial load and once by the load more event + assert(paginateLambda).isCalledExactly(2) } } @@ -162,11 +176,11 @@ class PollHistoryPresenterTest { ), ): PollHistoryPresenter { return PollHistoryPresenter( - room = room, appCoroutineScope = appCoroutineScope, sendPollResponseAction = sendPollResponseAction, endPollAction = endPollAction, pollHistoryItemFactory = pollHistoryItemFactory, + timelineProvider = LiveTimelineProvider(room), ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt index 5039d10467..053e1c1d37 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -149,6 +149,10 @@ val SemanticColors.bigIconDefaultBackgroundColor val SemanticColors.bigCheckmarkBorderColor get() = if (isLight) LightColorTokens.colorGray400 else DarkColorTokens.colorGray400 +@OptIn(CoreColorToken::class) +val SemanticColors.highlightedMessageBackgroundColor + get() = if (isLight) LightColorTokens.colorGreen300 else DarkColorTokens.colorGreen300 + @PreviewsDayNight @Composable internal fun ColorAliasesPreview() = ElementPreview { @@ -167,6 +171,8 @@ internal fun ColorAliasesPreview() = ElementPreview { "temporaryColorBgSpecial" to ElementTheme.colors.temporaryColorBgSpecial, "iconSuccessPrimaryBackground" to ElementTheme.colors.iconSuccessPrimaryBackground, "bigIconBackgroundColor" to ElementTheme.colors.bigIconDefaultBackgroundColor, + "bigCheckmarkBorderColor" to ElementTheme.colors.bigCheckmarkBorderColor, + "highlightedMessageBackgroundColor" to ElementTheme.colors.highlightedMessageBackgroundColor, ) ) } 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 5c28b84c01..33cd86c005 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 @@ -32,8 +32,8 @@ import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline 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.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import kotlinx.coroutines.flow.Flow @@ -98,7 +98,16 @@ interface MatrixRoom : Closeable { val syncUpdateFlow: StateFlow - val timeline: MatrixTimeline + /** + * The live timeline of the room. Must be used to send Event to a room. + */ + val liveTimeline: Timeline + + /** + * Create a new timeline, focused on the provided Event. + * Should not be used directly, see `TimelineController` to manage the various timelines. + */ + suspend fun timelineFocusedOnEvent(eventId: EventId): Result fun destroy() @@ -122,12 +131,6 @@ interface MatrixRoom : Closeable { suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result - suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List): Result - - suspend fun enterSpecialMode(eventId: EventId?): Result - - suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result - suspend fun redactEvent(eventId: EventId, reason: String? = null): Result suspend fun sendImage( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/errors/FocusEventException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/errors/FocusEventException.kt new file mode 100644 index 0000000000..51859db1fb --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/errors/FocusEventException.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.api.room.errors + +import io.element.android.libraries.matrix.api.core.EventId + +sealed class FocusEventException : Exception() { + data class InvalidEventId( + val eventId: String, + val err: String + ) : FocusEventException() + + data class EventNotFound( + val eventId: EventId + ) : FocusEventException() + + data class Other( + val msg: String + ) : FocusEventException() +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt deleted file mode 100644 index 25760afc45..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.libraries.matrix.api.timeline - -import io.element.android.libraries.matrix.api.core.EventId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow - -interface MatrixTimeline : AutoCloseable { - data class PaginationState( - val isBackPaginating: Boolean, - val hasMoreToLoadBackwards: Boolean, - val beginningOfRoomReached: Boolean, - ) { - val canBackPaginate = !isBackPaginating && hasMoreToLoadBackwards - - companion object { - val Initial = PaginationState( - isBackPaginating = false, - hasMoreToLoadBackwards = true, - beginningOfRoomReached = false - ) - } - } - - val paginationState: StateFlow - val timelineItems: Flow> - val membershipChangeEventReceived: Flow - - suspend fun paginateBackwards(requestSize: Int): Result - suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result - suspend fun fetchDetailsForEvent(eventId: EventId): Result - suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result -} 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 new file mode 100644 index 0000000000..6d0850db83 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.api.timeline + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.TransactionId +import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.MediaUploadHandler +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.Mention +import io.element.android.libraries.matrix.api.room.location.AssetType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import java.io.File + +interface Timeline : AutoCloseable { + data class PaginationStatus( + val isPaginating: Boolean, + val hasMoreToLoad: Boolean, + ) { + val canPaginate: Boolean = !isPaginating && hasMoreToLoad + } + + enum class PaginationDirection { + BACKWARDS, + FORWARDS + } + + val membershipChangeEventReceived: Flow + suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result + suspend fun paginate(direction: PaginationDirection): Result + fun paginationStatus(direction: PaginationDirection): StateFlow + val timelineItems: Flow> + + suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result + + suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List): Result + + suspend fun enterSpecialMode(eventId: EventId?): Result + + suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result + + suspend fun sendImage( + file: File, + thumbnailFile: File?, + imageInfo: ImageInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback? + ): Result + + suspend fun sendVideo( + file: File, + thumbnailFile: File?, + videoInfo: VideoInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback? + ): Result + + suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result + + suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result + + suspend fun toggleReaction(emoji: String, eventId: EventId): Result + + suspend fun forwardEvent(eventId: EventId, roomIds: List): Result + + suspend fun retrySendMessage(transactionId: TransactionId): Result + + suspend fun cancelSend(transactionId: TransactionId): Result + + /** + * Share a location message in the room. + * + * @param body A human readable textual representation of the location. + * @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`. + * Respectively: latitude, longitude, and (optional) uncertainty. + * @param description Optional description of the location to display to the user. + * @param zoomLevel Optional zoom level to display the map at. + * @param assetType Optional type of the location asset. + * Set to SENDER if sharing own location. Set to PIN if sharing any location. + */ + suspend fun sendLocation( + body: String, + geoUri: String, + description: String? = null, + zoomLevel: Int? = null, + assetType: AssetType? = null, + ): Result + + /** + * Create a poll in the room. + * + * @param question The question to ask. + * @param answers The list of answers. + * @param maxSelections The maximum number of answers that can be selected. + * @param pollKind The kind of poll to create. + */ + suspend fun createPoll( + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result + + /** + * Edit a poll in the room. + * + * @param pollStartId The event ID of the poll start event. + * @param question The question to ask. + * @param answers The list of answers. + * @param maxSelections The maximum number of answers that can be selected. + * @param pollKind The kind of poll to create. + */ + suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result + + /** + * Send a response to a poll. + * + * @param pollStartId The event ID of the poll start event. + * @param answers The list of answer ids to send. + */ + suspend fun sendPollResponse(pollStartId: EventId, answers: List): Result + + /** + * Ends a poll in the room. + * + * @param pollStartId The event ID of the poll start event. + * @param text Fallback text of the poll end event. + */ + suspend fun endPoll(pollStartId: EventId, text: String): Result + + suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback? + ): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt new file mode 100644 index 0000000000..ebfaca48be --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.api.timeline + +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first + +/** + * This interface defines a way to get the active timeline. + * It could be the current room timeline, or a timeline for a specific event. + */ +interface TimelineProvider { + fun activeTimelineFlow(): StateFlow +} + +suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().first() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt index 1337491ff8..bdf3c2f6dc 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt @@ -26,7 +26,7 @@ sealed interface InReplyTo { data class NotLoaded(val eventId: EventId) : InReplyTo /** The event details are pending to be fetched. We should **not** fetch them again. */ - data object Pending : InReplyTo + data class Pending(val eventId: EventId) : InReplyTo /** The event details are available. */ data class Ready( @@ -44,5 +44,8 @@ sealed interface InReplyTo { * If the reason for the failure is consistent on the server, we'd enter a loop * where we keep trying to fetch the same event. * */ - data object Error : InReplyTo + data class Error( + val eventId: EventId, + val message: String, + ) : InReplyTo } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt index 9839d35a27..13ecad636b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt @@ -16,6 +16,8 @@ package io.element.android.libraries.matrix.api.timeline.item.virtual +import io.element.android.libraries.matrix.api.timeline.Timeline + sealed interface VirtualTimelineItem { data class DayDivider( val timestamp: Long @@ -24,4 +26,13 @@ sealed interface VirtualTimelineItem { data object ReadMarker : VirtualTimelineItem data object EncryptedHistoryBanner : VirtualTimelineItem + + data object RoomBeginning : VirtualTimelineItem + + data object LastForwardIndicator : VirtualTimelineItem + + data class LoadingIndicator( + val direction: Timeline.PaginationDirection, + val timestamp: Long, + ) : VirtualTimelineItem } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/FocusEventException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/FocusEventException.kt new file mode 100644 index 0000000000..c2a4456f55 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/FocusEventException.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.impl.room + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.errors.FocusEventException +import org.matrix.rustcomponents.sdk.FocusEventException as RustFocusEventException + +fun Throwable.toFocusEventException(): Throwable { + return when (this) { + is RustFocusEventException -> { + when (this) { + is RustFocusEventException.InvalidEventId -> { + FocusEventException.InvalidEventId(eventId, err) + } + is RustFocusEventException.EventNotFound -> { + FocusEventException.EventNotFound(EventId(eventId)) + } + is RustFocusEventException.Other -> { + FocusEventException.Other(msg) + } + } + } + else -> { + this + } + } +} 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 97035f0583..1fe5d78627 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomAlias @@ -43,21 +44,15 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.roomNotificationSettings -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline 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.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings -import io.element.android.libraries.matrix.impl.core.toProgressWatcher -import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl -import io.element.android.libraries.matrix.impl.media.map -import io.element.android.libraries.matrix.impl.media.toMSC3246range import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService -import io.element.android.libraries.matrix.impl.poll.toInner -import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper -import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline +import io.element.android.libraries.matrix.impl.timeline.RustTimeline import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver @@ -80,24 +75,16 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.MessageFormat import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem -import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation -import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.TypingNotificationsListener import org.matrix.rustcomponents.sdk.UserPowerLevelUpdate import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider -import org.matrix.rustcomponents.sdk.messageEventContentFromHtml -import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import org.matrix.rustcomponents.sdk.use -import timber.log.Timber import uniffi.matrix_sdk.RoomPowerLevelChanges import java.io.File -import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody import org.matrix.rustcomponents.sdk.Room as InnerRoom import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @@ -107,7 +94,7 @@ class RustMatrixRoom( private val isKeyBackupEnabled: Boolean, private val roomListItem: RoomListItem, private val innerRoom: InnerRoom, - private val innerTimeline: InnerTimeline, + innerTimeline: InnerTimeline, private val roomNotificationSettingsService: RustNotificationSettingsService, sessionCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, @@ -160,7 +147,7 @@ class RustMatrixRoom( private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow - override val timeline = createMatrixTimeline(innerTimeline) { + override val liveTimeline = createTimeline(innerTimeline, isLive = true) { _syncUpdateFlow.value = systemClock.epochMillis() } @@ -170,7 +157,7 @@ class RustMatrixRoom( init { val powerLevelChanges = roomInfoFlow.map { it.userPowerLevels }.distinctUntilChanged() - val membershipChanges = timeline.membershipChangeEventReceived.onStart { emit(Unit) } + val membershipChanges = liveTimeline.membershipChangeEventReceived.onStart { emit(Unit) } combine(membershipChanges, powerLevelChanges) { _, _ -> } // Skip initial one .drop(1) @@ -183,12 +170,25 @@ class RustMatrixRoom( override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId) + override suspend fun timelineFocusedOnEvent(eventId: EventId): Result { + return runCatching { + innerRoom.timelineFocusedOnEvent( + eventId = eventId.value, + numContextEvents = 50u, + internalIdPrefix = "focus_$eventId", + ).let { inner -> + createTimeline(inner, isLive = false) + } + }.mapFailure { + it.toFocusEventException() + } + } + override fun destroy() { roomCoroutineScope.cancel() - timeline.close() + liveTimeline.close() innerRoom.destroy() roomListItem.destroy() - specialModeEventTimelineItem?.destroy() } override val name: String? @@ -322,59 +322,8 @@ class RustMatrixRoom( } } - override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result = withContext(roomDispatcher) { - messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content -> - runCatching { - innerTimeline.send(content) - } - } - } - - override suspend fun editMessage( - originalEventId: EventId?, - transactionId: TransactionId?, - body: String, - htmlBody: String?, - mentions: List, - ): Result = - withContext(roomDispatcher) { - if (originalEventId != null) { - runCatching { - val editedEvent = specialModeEventTimelineItem ?: innerTimeline.getEventTimelineItemByEventId(originalEventId.value) - editedEvent.use { - innerTimeline.edit( - newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), - editItem = it, - ) - } - specialModeEventTimelineItem = null - } - } else { - runCatching { - transactionId?.let { cancelSend(it) } - innerTimeline.send(messageEventContentFromParts(body, htmlBody)) - } - } - } - - private var specialModeEventTimelineItem: EventTimelineItem? = null - - override suspend fun enterSpecialMode(eventId: EventId?): Result = withContext(roomDispatcher) { - runCatching { - specialModeEventTimelineItem?.destroy() - specialModeEventTimelineItem = null - specialModeEventTimelineItem = eventId?.let { innerTimeline.getEventTimelineItemByEventId(it.value) } - } - } - - override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result = withContext(roomDispatcher) { - runCatching { - val inReplyTo = specialModeEventTimelineItem ?: innerTimeline.getEventTimelineItemByEventId(eventId.value) - inReplyTo.use { eventTimelineItem -> - innerTimeline.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem) - } - specialModeEventTimelineItem = null - } + override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result { + return liveTimeline.sendMessage(body, htmlBody, mentions) } override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) { @@ -457,18 +406,7 @@ class RustMatrixRoom( formattedBody: String?, progressCallback: ProgressCallback?, ): Result { - return sendAttachment(listOfNotNull(file, thumbnailFile)) { - innerTimeline.sendImage( - url = file.path, - thumbnailUrl = thumbnailFile?.path, - imageInfo = imageInfo.map(), - caption = body, - formattedCaption = formattedBody?.let { - RustFormattedBody(body = it, format = MessageFormat.Html) - }, - progressWatcher = progressCallback?.toProgressWatcher() - ) - } + return liveTimeline.sendImage(file, thumbnailFile, imageInfo, body, formattedBody, progressCallback) } override suspend fun sendVideo( @@ -479,63 +417,31 @@ class RustMatrixRoom( formattedBody: String?, progressCallback: ProgressCallback?, ): Result { - return sendAttachment(listOfNotNull(file, thumbnailFile)) { - innerTimeline.sendVideo( - url = file.path, - thumbnailUrl = thumbnailFile?.path, - videoInfo = videoInfo.map(), - caption = body, - formattedCaption = formattedBody?.let { - RustFormattedBody(body = it, format = MessageFormat.Html) - }, - progressWatcher = progressCallback?.toProgressWatcher() - ) - } + return liveTimeline.sendVideo(file, thumbnailFile, videoInfo, body, formattedBody, progressCallback) } override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result { - return sendAttachment(listOf(file)) { - innerTimeline.sendAudio( - url = file.path, - audioInfo = audioInfo.map(), - // Maybe allow a caption in the future? - caption = null, - formattedCaption = null, - progressWatcher = progressCallback?.toProgressWatcher() - ) - } + return liveTimeline.sendAudio(file, audioInfo, progressCallback) } override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result { - return sendAttachment(listOf(file)) { - innerTimeline.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher()) - } + return liveTimeline.sendFile(file, fileInfo, progressCallback) } - override suspend fun toggleReaction(emoji: String, eventId: EventId): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.toggleReaction(key = emoji, eventId = eventId.value) - } + override suspend fun toggleReaction(emoji: String, eventId: EventId): Result { + return liveTimeline.toggleReaction(emoji, eventId) } - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(roomDispatcher) { - runCatching { - roomContentForwarder.forward(fromTimeline = innerTimeline, eventId = eventId, toRoomIds = roomIds) - }.onFailure { - Timber.e(it) - } + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result { + return liveTimeline.forwardEvent(eventId, roomIds) } - override suspend fun retrySendMessage(transactionId: TransactionId): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.retrySend(transactionId.value) - } + override suspend fun retrySendMessage(transactionId: TransactionId): Result { + return liveTimeline.retrySendMessage(transactionId) } - override suspend fun cancelSend(transactionId: TransactionId): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.cancelSend(transactionId.value) - } + override suspend fun cancelSend(transactionId: TransactionId): Result { + return liveTimeline.cancelSend(transactionId) } override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = withContext(roomDispatcher) { @@ -613,16 +519,8 @@ class RustMatrixRoom( description: String?, zoomLevel: Int?, assetType: AssetType?, - ): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.sendLocation( - body = body, - geoUri = geoUri, - description = description, - zoomLevel = zoomLevel?.toUByte(), - assetType = assetType?.toInner(), - ) - } + ): Result { + return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType) } override suspend fun createPoll( @@ -630,15 +528,8 @@ class RustMatrixRoom( answers: List, maxSelections: Int, pollKind: PollKind, - ): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.createPoll( - question = question, - answers = answers, - maxSelections = maxSelections.toUByte(), - pollKind = pollKind.toInner(), - ) - } + ): Result { + return liveTimeline.createPoll(question, answers, maxSelections, pollKind) } override suspend fun editPoll( @@ -647,46 +538,22 @@ class RustMatrixRoom( answers: List, maxSelections: Int, pollKind: PollKind, - ): Result = withContext(roomDispatcher) { - runCatching { - val pollStartEvent = - innerTimeline.getEventTimelineItemByEventId( - eventId = pollStartId.value - ) - pollStartEvent.use { - innerTimeline.editPoll( - question = question, - answers = answers, - maxSelections = maxSelections.toUByte(), - pollKind = pollKind.toInner(), - editItem = pollStartEvent, - ) - } - } + ): Result { + return liveTimeline.editPoll(pollStartId, question, answers, maxSelections, pollKind) } override suspend fun sendPollResponse( pollStartId: EventId, answers: List - ): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.sendPollResponse( - pollStartId = pollStartId.value, - answers = answers, - ) - } + ): Result { + return liveTimeline.sendPollResponse(pollStartId, answers) } override suspend fun endPoll( pollStartId: EventId, text: String - ): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.endPoll( - pollStartId = pollStartId.value, - text = text, - ) - } + ): Result { + return liveTimeline.endPoll(pollStartId, text) } override suspend fun sendVoiceMessage( @@ -694,16 +561,8 @@ class RustMatrixRoom( audioInfo: AudioInfo, waveform: List, progressCallback: ProgressCallback?, - ): Result = sendAttachment(listOf(file)) { - innerTimeline.sendVoiceMessage( - url = file.path, - audioInfo = audioInfo.map(), - waveform = waveform.toMSC3246range(), - // Maybe allow a caption in the future? - caption = null, - formattedCaption = null, - progressWatcher = progressCallback?.toProgressWatcher(), - ) + ): Result { + return liveTimeline.sendVoiceMessage(file, audioInfo, waveform, progressCallback) } override suspend fun typingNotice(isTyping: Boolean) = runCatching { @@ -739,31 +598,22 @@ class RustMatrixRoom( innerRoom.matrixToEventPermalink(eventId.value) } - private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { - return runCatching { - MediaUploadHandlerImpl(files, handle()) - } - } - - private fun createMatrixTimeline( + private fun createTimeline( timeline: InnerTimeline, + isLive: Boolean, onNewSyncedEvent: () -> Unit = {}, - ): MatrixTimeline { - return RustMatrixTimeline( + ): Timeline { + return RustTimeline( isKeyBackupEnabled = isKeyBackupEnabled, + isLive = isLive, matrixRoom = this, + systemClock = systemClock, roomCoroutineScope = roomCoroutineScope, dispatcher = roomDispatcher, lastLoginTimestamp = sessionData.loginTimestamp, onNewSyncedEvent = onNewSyncedEvent, - innerTimeline = timeline, + roomContentForwarder = roomContentForwarder, + inner = timeline, ) } - - private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation = - if (htmlBody != null) { - messageEventContentFromHtml(body, htmlBody) - } else { - messageEventContentFromMarkdown(body) - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt deleted file mode 100644 index 1ec7e007e9..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.libraries.matrix.impl.timeline - -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.ReceiptType -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import timber.log.Timber - -/** - * This class is a wrapper around a [MatrixTimeline] that will be created asynchronously. - */ -@Suppress("unused") -class AsyncMatrixTimeline( - coroutineScope: CoroutineScope, - dispatcher: CoroutineDispatcher, - private val timelineProvider: suspend () -> MatrixTimeline -) : MatrixTimeline { - private val _timelineItems: MutableStateFlow> = - MutableStateFlow(emptyList()) - - private val _paginationState = MutableStateFlow( - MatrixTimeline.PaginationState.Initial - ) - private val timeline = coroutineScope.async(context = dispatcher, start = CoroutineStart.LAZY) { - timelineProvider() - } - private val closeSignal = CompletableDeferred() - - override val membershipChangeEventReceived = MutableSharedFlow(extraBufferCapacity = 1) - - init { - coroutineScope.launch { - val delegateTimeline = timeline.await() - delegateTimeline.timelineItems - .onEach { _timelineItems.value = it } - .launchIn(this) - delegateTimeline.paginationState - .onEach { _paginationState.value = it } - .launchIn(this) - delegateTimeline.membershipChangeEventReceived - .onEach { membershipChangeEventReceived.emit(it) } - .launchIn(this) - - launch { - withContext(NonCancellable) { - closeSignal.await() - Timber.d("Close delegate") - delegateTimeline.close() - } - } - } - } - - override val paginationState: StateFlow = _paginationState - override val timelineItems: Flow> = _timelineItems - - override suspend fun paginateBackwards(requestSize: Int): Result { - return timeline.await().paginateBackwards(requestSize) - } - - override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result { - return timeline.await().paginateBackwards(requestSize, untilNumberOfItems) - } - - override suspend fun fetchDetailsForEvent(eventId: EventId): Result { - return timeline.await().fetchDetailsForEvent(eventId) - } - - override suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result { - return timeline.await().sendReadReceipt(eventId, receiptType) - } - - override fun close() { - closeSignal.complete(Unit) - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt index 6dfed57cec..381bfd8c95 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt @@ -16,10 +16,8 @@ package io.element.android.libraries.matrix.impl.timeline -import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import io.element.android.libraries.matrix.impl.util.destroyAll -import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking @@ -27,13 +25,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.catch -import org.matrix.rustcomponents.sdk.PaginationStatusListener import org.matrix.rustcomponents.sdk.Timeline import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineListener import timber.log.Timber -import uniffi.matrix_sdk_ui.PaginationStatus internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List) -> Unit): Flow> = callbackFlow { @@ -58,18 +54,6 @@ internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List = - mxCallbackFlow { - val listener = object : PaginationStatusListener { - override fun onUpdate(status: PaginationStatus) { - trySendBlocking(status) - } - } - tryOrNull { - subscribeToBackPaginationStatus(listener) - } - }.buffer(Channel.UNLIMITED) - internal suspend fun Timeline.runWithTimelineListenerRegistered(action: suspend () -> Unit) { val result = addListener(NoOpTimelineListener) try { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt deleted file mode 100644 index 68043f2565..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ /dev/null @@ -1,278 +0,0 @@ -/* - * 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.libraries.matrix.impl.timeline - -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.matrix.api.timeline.TimelineException -import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem -import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper -import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper -import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper -import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper -import io.element.android.libraries.matrix.impl.timeline.postprocessor.DmBeginningTimelineProcessor -import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.PaginationOptions -import org.matrix.rustcomponents.sdk.Timeline -import org.matrix.rustcomponents.sdk.TimelineDiff -import org.matrix.rustcomponents.sdk.TimelineItem -import timber.log.Timber -import uniffi.matrix_sdk_ui.EventItemOrigin -import uniffi.matrix_sdk_ui.PaginationStatus -import java.util.Date -import java.util.concurrent.atomic.AtomicBoolean - -private const val INITIAL_MAX_SIZE = 50 - -class RustMatrixTimeline( - roomCoroutineScope: CoroutineScope, - isKeyBackupEnabled: Boolean, - private val matrixRoom: MatrixRoom, - private val innerTimeline: Timeline, - private val dispatcher: CoroutineDispatcher, - lastLoginTimestamp: Date?, - private val onNewSyncedEvent: () -> Unit, -) : MatrixTimeline { - private val initLatch = CompletableDeferred() - private val isInit = AtomicBoolean(false) - - private val _timelineItems: MutableStateFlow> = - MutableStateFlow(emptyList()) - - private val _paginationState = MutableStateFlow( - MatrixTimeline.PaginationState.Initial - ) - - private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor( - lastLoginTimestamp = lastLoginTimestamp, - isRoomEncrypted = matrixRoom.isEncrypted, - isKeyBackupEnabled = isKeyBackupEnabled, - dispatcher = dispatcher, - ) - - private val dmBeginningTimelineProcessor = DmBeginningTimelineProcessor() - - private val timelineItemFactory = MatrixTimelineItemMapper( - fetchDetailsForEvent = this::fetchDetailsForEvent, - roomCoroutineScope = roomCoroutineScope, - virtualTimelineItemMapper = VirtualTimelineItemMapper(), - eventTimelineItemMapper = EventTimelineItemMapper( - contentMapper = TimelineEventContentMapper( - eventMessageMapper = EventMessageMapper() - ) - ) - ) - - private val timelineDiffProcessor = MatrixTimelineDiffProcessor( - timelineItems = _timelineItems, - timelineItemFactory = timelineItemFactory, - ) - - override val paginationState: StateFlow = _paginationState.asStateFlow() - - @OptIn(ExperimentalCoroutinesApi::class) - override val timelineItems: Flow> = _timelineItems - .mapLatest { items -> encryptedHistoryPostProcessor.process(items) } - .mapLatest { items -> - dmBeginningTimelineProcessor.process( - items = items, - isDm = matrixRoom.isDirect && matrixRoom.isOneToOne, - isAtStartOfTimeline = paginationState.value.beginningOfRoomReached - ) - } - - override val membershipChangeEventReceived: Flow = timelineDiffProcessor.membershipChangeEventReceived - - init { - Timber.d("Initialize timeline for room ${matrixRoom.roomId}") - - roomCoroutineScope.launch(dispatcher) { - innerTimeline.timelineDiffFlow { initialList -> - postItems(initialList) - }.onEach { diffs -> - if (diffs.any { diff -> diff.eventOrigin() == EventItemOrigin.SYNC }) { - onNewSyncedEvent() - } - postDiffs(diffs) - }.launchIn(this) - - paginationStateFlow() - .onEach { - _paginationState.value = it - } - .launchIn(this) - - fetchMembers() - } - } - - private fun paginationStateFlow(): Flow { - return combine( - innerTimeline.backPaginationStatusFlow(), - timelineItems, - ) { paginationStatus, filteredItems -> - if (filteredItems.hasEncryptionHistoryBanner()) { - return@combine MatrixTimeline.PaginationState( - isBackPaginating = false, - hasMoreToLoadBackwards = false, - beginningOfRoomReached = false, - ) - } - when (paginationStatus) { - PaginationStatus.IDLE -> { - MatrixTimeline.PaginationState( - isBackPaginating = false, - hasMoreToLoadBackwards = true, - beginningOfRoomReached = false, - ) - } - PaginationStatus.PAGINATING -> { - MatrixTimeline.PaginationState( - isBackPaginating = true, - hasMoreToLoadBackwards = true, - beginningOfRoomReached = false, - ) - } - PaginationStatus.TIMELINE_END_REACHED -> { - MatrixTimeline.PaginationState( - isBackPaginating = false, - hasMoreToLoadBackwards = false, - beginningOfRoomReached = true, - ) - } - } - } - } - - private suspend fun fetchMembers() = withContext(dispatcher) { - initLatch.await() - try { - innerTimeline.fetchMembers() - } catch (exception: Exception) { - Timber.e(exception, "Error fetching members for room ${matrixRoom.roomId}") - } - } - - private suspend fun postItems(items: List) = coroutineScope { - // Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap. - items.chunked(INITIAL_MAX_SIZE).reversed().forEach { - ensureActive() - timelineDiffProcessor.postItems(it) - } - isInit.set(true) - initLatch.complete(Unit) - } - - private suspend fun postDiffs(diffs: List) { - initLatch.await() - timelineDiffProcessor.postDiffs(diffs) - } - - override suspend fun fetchDetailsForEvent(eventId: EventId): Result = withContext(dispatcher) { - runCatching { - innerTimeline.fetchDetailsForEvent(eventId.value) - } - } - - override suspend fun paginateBackwards(requestSize: Int): Result { - val paginationOptions = PaginationOptions.SimpleRequest( - eventLimit = requestSize.toUShort(), - waitForToken = true, - ) - return paginateBackwards(paginationOptions) - } - - override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result { - val paginationOptions = PaginationOptions.UntilNumItems( - eventLimit = requestSize.toUShort(), - items = untilNumberOfItems.toUShort(), - waitForToken = true, - ) - return paginateBackwards(paginationOptions) - } - - private suspend fun paginateBackwards(paginationOptions: PaginationOptions): Result = withContext(dispatcher) { - initLatch.await() - runCatching { - if (!canBackPaginate()) throw TimelineException.CannotPaginate - Timber.v("Start back paginating for room ${matrixRoom.roomId} ") - innerTimeline.paginateBackwards( - when (paginationOptions) { - is PaginationOptions.SimpleRequest -> paginationOptions.eventLimit - is PaginationOptions.UntilNumItems -> paginationOptions.eventLimit - } - ) - }.onFailure { error -> - if (error is TimelineException.CannotPaginate) { - Timber.d("Can't paginate backwards on room ${matrixRoom.roomId}, we're already at the start") - } else { - Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}") - } - }.onSuccess { - Timber.v("Success back paginating for room ${matrixRoom.roomId}") - }.map { } - } - - private fun canBackPaginate(): Boolean { - return isInit.get() && paginationState.value.canBackPaginate - } - - override suspend fun sendReadReceipt( - eventId: EventId, - receiptType: ReceiptType, - ) = withContext(dispatcher) { - runCatching { - innerTimeline.sendReadReceipt( - receiptType = receiptType.toRustReceiptType(), - eventId = eventId.value, - ) - } - } - - override fun close() { - innerTimeline.close() - } - - fun getItemById(eventId: EventId): MatrixTimelineItem.Event? { - return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event - } - - private fun List.hasEncryptionHistoryBanner(): Boolean { - val firstItem = firstOrNull() - return firstItem is MatrixTimelineItem.Virtual && - firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner - } -} 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 new file mode 100644 index 0000000000..3ec21e7560 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -0,0 +1,523 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.impl.timeline + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.TransactionId +import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.MediaUploadHandler +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.MatrixRoom +import io.element.android.libraries.matrix.api.room.Mention +import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +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.impl.core.toProgressWatcher +import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl +import io.element.android.libraries.matrix.impl.media.map +import io.element.android.libraries.matrix.impl.media.toMSC3246range +import io.element.android.libraries.matrix.impl.poll.toInner +import io.element.android.libraries.matrix.impl.room.RoomContentForwarder +import io.element.android.libraries.matrix.impl.room.location.toInner +import io.element.android.libraries.matrix.impl.room.map +import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper +import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper +import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper +import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper +import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor +import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor +import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor +import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.EventTimelineItem +import org.matrix.rustcomponents.sdk.FormattedBody +import org.matrix.rustcomponents.sdk.MessageFormat +import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation +import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineItem +import org.matrix.rustcomponents.sdk.messageEventContentFromHtml +import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown +import org.matrix.rustcomponents.sdk.use +import timber.log.Timber +import uniffi.matrix_sdk_ui.EventItemOrigin +import java.io.File +import java.util.Date +import java.util.concurrent.atomic.AtomicBoolean +import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline + +private const val INITIAL_MAX_SIZE = 50 +private const val PAGINATION_SIZE = 50 + +class RustTimeline( + private val inner: InnerTimeline, + isLive: Boolean, + systemClock: SystemClock, + roomCoroutineScope: CoroutineScope, + isKeyBackupEnabled: Boolean, + private val matrixRoom: MatrixRoom, + private val dispatcher: CoroutineDispatcher, + lastLoginTimestamp: Date?, + private val roomContentForwarder: RoomContentForwarder, + private val onNewSyncedEvent: () -> Unit, +) : Timeline { + private val initLatch = CompletableDeferred() + private val isInit = AtomicBoolean(false) + + private val _timelineItems: MutableStateFlow> = + MutableStateFlow(emptyList()) + + private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor( + lastLoginTimestamp = lastLoginTimestamp, + isRoomEncrypted = matrixRoom.isEncrypted, + isKeyBackupEnabled = isKeyBackupEnabled, + dispatcher = dispatcher, + ) + + private val roomBeginningPostProcessor = RoomBeginningPostProcessor() + private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock) + private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(isLive) + + private val timelineItemFactory = MatrixTimelineItemMapper( + fetchDetailsForEvent = this::fetchDetailsForEvent, + roomCoroutineScope = roomCoroutineScope, + virtualTimelineItemMapper = VirtualTimelineItemMapper(), + eventTimelineItemMapper = EventTimelineItemMapper( + contentMapper = TimelineEventContentMapper( + eventMessageMapper = EventMessageMapper() + ) + ) + ) + + private val timelineDiffProcessor = MatrixTimelineDiffProcessor( + timelineItems = _timelineItems, + timelineItemFactory = timelineItemFactory, + ) + + private val backPaginationStatus = MutableStateFlow( + Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true) + ) + + private val forwardPaginationStatus = MutableStateFlow( + Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = !isLive) + ) + + init { + roomCoroutineScope.launch(dispatcher) { + inner.timelineDiffFlow { initialList -> + postItems(initialList) + }.onEach { diffs -> + if (diffs.any { diff -> diff.eventOrigin() == EventItemOrigin.SYNC }) { + onNewSyncedEvent() + } + postDiffs(diffs) + }.launchIn(this) + + launch { + fetchMembers() + } + } + } + + override val membershipChangeEventReceived: Flow = timelineDiffProcessor.membershipChangeEventReceived + + override suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result { + return runCatching { + inner.sendReadReceipt(receiptType.toRustReceiptType(), eventId.value) + } + } + + private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) { + when (direction) { + Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.getAndUpdate(update) + Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update) + } + } + + // Use NonCancellable to avoid breaking the timeline when the coroutine is cancelled. + override suspend fun paginate(direction: Timeline.PaginationDirection): Result = withContext(NonCancellable) { + initLatch.await() + runCatching { + if (!canPaginate(direction)) throw TimelineException.CannotPaginate + updatePaginationStatus(direction) { it.copy(isPaginating = true) } + when (direction) { + Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(PAGINATION_SIZE.toUShort()) + Timeline.PaginationDirection.FORWARDS -> inner.focusedPaginateForwards(PAGINATION_SIZE.toUShort()) + } + }.onFailure { error -> + updatePaginationStatus(direction) { it.copy(isPaginating = false) } + if (error is TimelineException.CannotPaginate) { + Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}") + } else { + Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}") + } + }.onSuccess { hasReachedEnd -> + updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) } + } + } + + private fun canPaginate(direction: Timeline.PaginationDirection): Boolean { + if (!isInit.get()) return false + return when (direction) { + Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.value.canPaginate + Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate + } + } + + override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow { + return when (direction) { + Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus + Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus + } + } + + override val timelineItems: Flow> = combine( + _timelineItems, + backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(), + forwardPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(), + ) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward -> + timelineItems + .let { items -> encryptedHistoryPostProcessor.process(items) } + .let { items -> + roomBeginningPostProcessor.process( + items = items, + isDm = matrixRoom.isDm, + hasMoreToLoadBackwards = hasMoreToLoadBackward + ) + } + .let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) } + // Keep lastForwardIndicatorsPostProcessor last + .let { items -> lastForwardIndicatorsPostProcessor.process(items) } + } + + override fun close() { + inner.close() + specialModeEventTimelineItem?.destroy() + } + + private suspend fun fetchMembers() = withContext(dispatcher) { + initLatch.await() + try { + inner.fetchMembers() + } catch (exception: Exception) { + Timber.e(exception, "Error fetching members for room ${matrixRoom.roomId}") + } + } + + private suspend fun postItems(items: List) = coroutineScope { + // Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap. + items.chunked(INITIAL_MAX_SIZE).reversed().forEach { + ensureActive() + timelineDiffProcessor.postItems(it) + } + isInit.set(true) + initLatch.complete(Unit) + } + + private suspend fun postDiffs(diffs: List) { + initLatch.await() + timelineDiffProcessor.postDiffs(diffs) + } + + override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result = withContext(dispatcher) { + messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content -> + runCatching { + inner.send(content) + } + } + } + + override suspend fun editMessage( + originalEventId: EventId?, + transactionId: TransactionId?, + body: String, + htmlBody: String?, + mentions: List, + ): Result = + withContext(dispatcher) { + if (originalEventId != null) { + runCatching { + val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value) + editedEvent.use { + inner.edit( + newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), + editItem = it, + ) + } + specialModeEventTimelineItem = null + } + } else { + runCatching { + transactionId?.let { cancelSend(it) } + inner.send(messageEventContentFromParts(body, htmlBody)) + } + } + } + + private var specialModeEventTimelineItem: EventTimelineItem? = null + + override suspend fun enterSpecialMode(eventId: EventId?): Result = withContext(dispatcher) { + runCatching { + specialModeEventTimelineItem?.destroy() + specialModeEventTimelineItem = null + specialModeEventTimelineItem = eventId?.let { inner.getEventTimelineItemByEventId(it.value) } + }.onFailure { + Timber.e(it, "Unable to retrieve event for special mode. Are you using the correct timeline?") + } + } + + override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result = withContext(dispatcher) { + runCatching { + val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value) + inReplyTo.use { eventTimelineItem -> + inner.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem) + } + specialModeEventTimelineItem = null + } + } + + override suspend fun sendImage( + file: File, + thumbnailFile: File?, + imageInfo: ImageInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ): Result { + return sendAttachment(listOfNotNull(file, thumbnailFile)) { + inner.sendImage( + url = file.path, + thumbnailUrl = thumbnailFile?.path, + imageInfo = imageInfo.map(), + caption = body, + formattedCaption = formattedBody?.let { + FormattedBody(body = it, format = MessageFormat.Html) + }, + progressWatcher = progressCallback?.toProgressWatcher() + ) + } + } + + override suspend fun sendVideo( + file: File, + thumbnailFile: File?, + videoInfo: VideoInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ): Result { + return sendAttachment(listOfNotNull(file, thumbnailFile)) { + inner.sendVideo( + url = file.path, + thumbnailUrl = thumbnailFile?.path, + videoInfo = videoInfo.map(), + caption = body, + formattedCaption = formattedBody?.let { + FormattedBody(body = it, format = MessageFormat.Html) + }, + progressWatcher = progressCallback?.toProgressWatcher() + ) + } + } + + override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result { + return sendAttachment(listOf(file)) { + inner.sendAudio( + url = file.path, + audioInfo = audioInfo.map(), + // Maybe allow a caption in the future? + caption = null, + formattedCaption = null, + progressWatcher = progressCallback?.toProgressWatcher() + ) + } + } + + override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result { + return sendAttachment(listOf(file)) { + inner.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher()) + } + } + + override suspend fun toggleReaction(emoji: String, eventId: EventId): Result = withContext(dispatcher) { + runCatching { + inner.toggleReaction(key = emoji, eventId = eventId.value) + } + } + + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(dispatcher) { + runCatching { + roomContentForwarder.forward(fromTimeline = inner, eventId = eventId, toRoomIds = roomIds) + }.onFailure { + Timber.e(it) + } + } + + override suspend fun retrySendMessage(transactionId: TransactionId): Result = withContext(dispatcher) { + runCatching { + inner.retrySend(transactionId.value) + } + } + + override suspend fun cancelSend(transactionId: TransactionId): Result = withContext(dispatcher) { + runCatching { + inner.cancelSend(transactionId.value) + } + } + + override suspend fun sendLocation( + body: String, + geoUri: String, + description: String?, + zoomLevel: Int?, + assetType: AssetType?, + ): Result = withContext(dispatcher) { + runCatching { + inner.sendLocation( + body = body, + geoUri = geoUri, + description = description, + zoomLevel = zoomLevel?.toUByte(), + assetType = assetType?.toInner(), + ) + } + } + + override suspend fun createPoll( + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result = withContext(dispatcher) { + runCatching { + inner.createPoll( + question = question, + answers = answers, + maxSelections = maxSelections.toUByte(), + pollKind = pollKind.toInner(), + ) + } + } + + override suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result = withContext(dispatcher) { + runCatching { + val pollStartEvent = + inner.getEventTimelineItemByEventId( + eventId = pollStartId.value + ) + pollStartEvent.use { + inner.editPoll( + question = question, + answers = answers, + maxSelections = maxSelections.toUByte(), + pollKind = pollKind.toInner(), + editItem = pollStartEvent, + ) + } + } + } + + override suspend fun sendPollResponse( + pollStartId: EventId, + answers: List + ): Result = withContext(dispatcher) { + runCatching { + inner.sendPollResponse( + pollStartId = pollStartId.value, + answers = answers, + ) + } + } + + override suspend fun endPoll( + pollStartId: EventId, + text: String + ): Result = withContext(dispatcher) { + runCatching { + inner.endPoll( + pollStartId = pollStartId.value, + text = text, + ) + } + } + + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + ): Result = sendAttachment(listOf(file)) { + inner.sendVoiceMessage( + url = file.path, + audioInfo = audioInfo.map(), + waveform = waveform.toMSC3246range(), + // Maybe allow a caption in the future? + caption = null, + formattedCaption = null, + progressWatcher = progressCallback?.toProgressWatcher(), + ) + } + + private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation = + if (htmlBody != null) { + messageEventContentFromHtml(body, htmlBody) + } else { + messageEventContentFromMarkdown(body) + } + + private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { + return runCatching { + MediaUploadHandlerImpl(files, handle()) + } + } + + private fun fetchDetailsForEvent(eventId: EventId): Result { + return runCatching { + inner.fetchDetailsForEvent(eventId.value) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index d3a9294cc2..09a66fa656 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -57,9 +57,16 @@ class EventMessageMapper { senderProfile = event.senderProfile.map(), ) } - is RepliedToEventDetails.Error -> InReplyTo.Error - is RepliedToEventDetails.Pending -> InReplyTo.Pending - is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded(inReplyToId) + is RepliedToEventDetails.Error -> InReplyTo.Error( + eventId = inReplyToId, + message = event.message, + ) + RepliedToEventDetails.Pending -> InReplyTo.Pending( + eventId = inReplyToId, + ) + is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded( + eventId = inReplyToId + ) } } MessageContent( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/HasEncryptionHistoryBanner.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/HasEncryptionHistoryBanner.kt new file mode 100644 index 0000000000..2d6801918e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/HasEncryptionHistoryBanner.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.impl.timeline.postprocessor + +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem + +internal fun List.hasEncryptionHistoryBanner(): Boolean { + val firstItem = firstOrNull() + return firstItem is MatrixTimelineItem.Virtual && + firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt new file mode 100644 index 0000000000..d717fac00d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.impl.timeline.postprocessor + +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem + +/** + * This post processor is responsible for adding virtual items to indicate all the previous last forward item. + */ +class LastForwardIndicatorsPostProcessor( + private val isTimelineLive: Boolean, +) { + private val lastForwardIdentifiers = LinkedHashSet() + + fun process( + items: List, + ): List { + // If the timeline is live, we don't have any last forward indicator to display + if (isTimelineLive) { + return items + } else { + return buildList { + val latestEventIdentifier = items.latestEventIdentifier() + // Remove if it always exists (this should happen only when no new events are added) + lastForwardIdentifiers.remove(latestEventIdentifier) + + items.forEach { item -> + add(item) + + if (item is MatrixTimelineItem.Event) { + if (lastForwardIdentifiers.contains(item.uniqueId)) { + add(createLastForwardIndicator(item.uniqueId)) + } + } + } + // This is important to always add this one at the end of the list so it's used to keep the scroll position. + add(createLastForwardIndicator(latestEventIdentifier)) + lastForwardIdentifiers.add(latestEventIdentifier) + } + } + } +} + +private fun createLastForwardIndicator(identifier: String): MatrixTimelineItem { + return MatrixTimelineItem.Virtual( + uniqueId = "last_forward_indicator_$identifier", + virtual = VirtualTimelineItem.LastForwardIndicator + ) +} + +private fun List.latestEventIdentifier(): String { + return findLast { + it is MatrixTimelineItem.Event + }?.let { + (it as MatrixTimelineItem.Event).uniqueId + } ?: "fake_id" +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt new file mode 100644 index 0000000000..7e24f89c1c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.impl.timeline.postprocessor + +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.item.virtual.VirtualTimelineItem +import io.element.android.services.toolbox.api.systemclock.SystemClock + +class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) { + fun process( + items: List, + hasMoreToLoadBackward: Boolean, + hasMoreToLoadForward: Boolean, + ): List { + val shouldAddBackwardLoadingIndicator = hasMoreToLoadBackward && !items.hasEncryptionHistoryBanner() + val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty() + val currentTimestamp = systemClock.epochMillis() + return buildList { + if (shouldAddBackwardLoadingIndicator) { + val backwardLoadingIndicator = MatrixTimelineItem.Virtual( + uniqueId = "BackwardLoadingIndicator", + virtual = VirtualTimelineItem.LoadingIndicator( + direction = Timeline.PaginationDirection.BACKWARDS, + timestamp = currentTimestamp + ) + ) + add(backwardLoadingIndicator) + } + addAll(items) + if (shouldAddForwardLoadingIndicator) { + val forwardLoadingIndicator = MatrixTimelineItem.Virtual( + uniqueId = "ForwardLoadingIndicator", + virtual = VirtualTimelineItem.LoadingIndicator( + direction = Timeline.PaginationDirection.FORWARDS, + timestamp = currentTimestamp + ) + ) + add(forwardLoadingIndicator) + } + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt similarity index 69% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessor.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index f36b4a78b8..e3fa9c8072 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -16,23 +16,38 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor +import androidx.annotation.VisibleForTesting import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem /** - * This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs. + * This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs + * or add the RoomBeginning item for non DM room. */ -class DmBeginningTimelineProcessor { +class RoomBeginningPostProcessor { fun process( items: List, isDm: Boolean, - isAtStartOfTimeline: Boolean + hasMoreToLoadBackwards: Boolean ): List { - if (!isDm || !isAtStartOfTimeline) return items + return when { + hasMoreToLoadBackwards -> items + isDm -> processForDM(items) + else -> processForRoom(items) + } + } + private fun processForRoom(items: List): List { + if (items.hasEncryptionHistoryBanner()) return items + val roomBeginningItem = createRoomBeginningItem() + return listOf(roomBeginningItem) + items + } + + private fun processForDM(items: List): List { // Find room creation event. This is usually index 0 val roomCreationEventIndex = items.indexOfFirst { val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? StateContent @@ -58,4 +73,12 @@ class DmBeginningTimelineProcessor { } return newItems } + + @VisibleForTesting + fun createRoomBeginningItem(): MatrixTimelineItem.Virtual { + return MatrixTimelineItem.Virtual( + uniqueId = VirtualTimelineItem.RoomBeginning.toString(), + virtual = VirtualTimelineItem.RoomBeginning + ) + } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt similarity index 79% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessorTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt index fc24f54c14..2c72481a25 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt @@ -22,21 +22,22 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem 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.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import org.junit.Test -class DmBeginningTimelineProcessorTest { +class RoomBeginningPostProcessorTest { @Test fun `processor removes room creation event and self-join event from DM timeline`() { val timelineItems = listOf( MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), ) - val processor = DmBeginningTimelineProcessor() - val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = true) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) assertThat(processedItems).isEmpty() } @@ -52,19 +53,31 @@ class DmBeginningTimelineProcessorTest { MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))), MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))), ) - val processor = DmBeginningTimelineProcessor() - val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = true) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo(expected) } @Test - fun `processor won't remove items if it's not a DM`() { + fun `processor will add beginning of room item if it's not a DM`() { val timelineItems = listOf( MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), ) - val processor = DmBeginningTimelineProcessor() - val processedItems = processor.process(timelineItems, isDm = false, isAtStartOfTimeline = true) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false) + assertThat(processedItems).isEqualTo( + listOf(processor.createRoomBeginningItem()) + timelineItems + ) + } + + @Test + fun `processor will not add beginning of room item if it's not a DM and EncryptedHistoryBanner item is found`() { + val timelineItems = listOf( + MatrixTimelineItem.Virtual("EncryptedHistoryBanner", VirtualTimelineItem.EncryptedHistoryBanner), + ) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo(timelineItems) } @@ -74,8 +87,8 @@ class DmBeginningTimelineProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), ) - val processor = DmBeginningTimelineProcessor() - val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -84,8 +97,8 @@ class DmBeginningTimelineProcessorTest { val timelineItems = listOf( MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), ) - val processor = DmBeginningTimelineProcessor() - val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -95,8 +108,8 @@ class DmBeginningTimelineProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))), ) - val processor = DmBeginningTimelineProcessor() - val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } } 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 830ac3a6e2..a435a7a4ca 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 @@ -43,8 +43,8 @@ import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline 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.event.EventTimelineItem import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings @@ -54,7 +54,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService -import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.tests.testutils.simulateLongTask import kotlinx.collections.immutable.ImmutableMap @@ -84,7 +84,7 @@ class FakeMatrixRoom( override val joinedMemberCount: Long = 123L, override val activeMemberCount: Long = 234L, val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), - private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), + override val liveTimeline: Timeline = FakeTimeline(), private var roomPermalinkResult: () -> Result = { Result.success("room link") }, private var eventPermalinkResult: (EventId) -> Result = { Result.success("event link") }, canRedactOwn: Boolean = false, @@ -134,7 +134,6 @@ class FakeMatrixRoom( private var updatePowerLevelsResult = Result.success(Unit) private var resetPowerLevelsResult = Result.success(defaultRoomPowerLevels()) var sendMessageMentions = emptyList() - val editMessageCalls = mutableListOf>() private val _typingRecord = mutableListOf() val typingRecord: List get() = _typingRecord @@ -215,7 +214,15 @@ class FakeMatrixRoom( override val syncUpdateFlow: StateFlow = MutableStateFlow(0L) - override val timeline: MatrixTimeline = matrixTimeline + private var timelineFocusedOnEventResult: Result = Result.success(FakeTimeline()) + + fun givenTimelineFocusedOnEventResult(result: Result) { + timelineFocusedOnEventResult = result + } + + override suspend fun timelineFocusedOnEvent(eventId: EventId): Result = simulateLongTask { + timelineFocusedOnEventResult + } override suspend fun subscribeToSync() = Unit @@ -288,31 +295,6 @@ class FakeMatrixRoom( return eventPermalinkResult(eventId) } - override suspend fun editMessage( - originalEventId: EventId?, - transactionId: TransactionId?, - body: String, - htmlBody: String?, - mentions: List - ): Result { - sendMessageMentions = mentions - editMessageCalls += body to htmlBody - return Result.success(Unit) - } - - var replyMessageParameter: Pair? = null - private set - - override suspend fun enterSpecialMode(eventId: EventId?): Result { - return Result.success(Unit) - } - - override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result { - sendMessageMentions = mentions - replyMessageParameter = body to htmlBody - return Result.success(Unit) - } - var redactEventEventIdParam: EventId? = null private set diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt deleted file mode 100644 index f9ab698b8e..0000000000 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.libraries.matrix.test.timeline - -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.tests.testutils.simulateLongTask -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.getAndUpdate - -class FakeMatrixTimeline( - initialTimelineItems: List = emptyList(), - initialPaginationState: MatrixTimeline.PaginationState = MatrixTimeline.PaginationState( - hasMoreToLoadBackwards = true, - isBackPaginating = false, - beginningOfRoomReached = false, - ) -) : MatrixTimeline { - private val _paginationState: MutableStateFlow = MutableStateFlow(initialPaginationState) - private val _timelineItems: MutableStateFlow> = MutableStateFlow(initialTimelineItems) - - var sentReadReceipts = mutableListOf>() - private set - - var sendReadReceiptLatch: CompletableDeferred? = null - - fun updatePaginationState(update: (MatrixTimeline.PaginationState.() -> MatrixTimeline.PaginationState)) { - _paginationState.getAndUpdate(update) - } - - fun updateTimelineItems(update: (items: List) -> List) { - _timelineItems.getAndUpdate(update) - } - - override val paginationState: StateFlow = _paginationState - - override val timelineItems: Flow> = _timelineItems - - override suspend fun paginateBackwards(requestSize: Int) = paginateBackwards() - override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int) = paginateBackwards() - - override val membershipChangeEventReceived = MutableSharedFlow() - - private suspend fun paginateBackwards(): Result { - updatePaginationState { - copy(isBackPaginating = true) - } - delay(100) - updatePaginationState { - copy(isBackPaginating = false) - } - updateTimelineItems { timelineItems -> - timelineItems - } - return Result.success(Unit) - } - - fun givenMembershipChangeEventReceived() { - membershipChangeEventReceived.tryEmit(Unit) - } - - override suspend fun fetchDetailsForEvent(eventId: EventId): Result = simulateLongTask { - Result.success(Unit) - } - - override suspend fun sendReadReceipt( - eventId: EventId, - receiptType: ReceiptType, - ): Result = simulateLongTask { - sentReadReceipts.add(eventId to receiptType) - sendReadReceiptLatch?.complete(Unit) - Result.success(Unit) - } - - override fun close() = Unit -} 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 new file mode 100644 index 0000000000..6ec8386651 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -0,0 +1,372 @@ +/* + * 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.libraries.matrix.test.timeline + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.TransactionId +import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.MediaUploadHandler +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.Mention +import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import java.io.File + +class FakeTimeline( + private val name: String = "FakeTimeline", + override val timelineItems: Flow> = MutableStateFlow(emptyList()), + private val backwardPaginationStatus: MutableStateFlow = MutableStateFlow( + Timeline.PaginationStatus( + isPaginating = false, + hasMoreToLoad = true + ) + ), + private val forwardPaginationStatus: MutableStateFlow = MutableStateFlow( + Timeline.PaginationStatus( + isPaginating = false, + hasMoreToLoad = false + ) + ), + override val membershipChangeEventReceived: Flow = MutableSharedFlow(), +) : Timeline { + var sendMessageLambda: ( + body: String, + htmlBody: String?, + mentions: List, + ) -> Result = { _, _, _ -> + Result.success(Unit) + } + + override suspend fun sendMessage( + body: String, + htmlBody: String?, + mentions: List, + ): Result = sendMessageLambda(body, htmlBody, mentions) + + var editMessageLambda: ( + originalEventId: EventId?, + transactionId: TransactionId?, + body: String, + htmlBody: String?, + mentions: List, + ) -> Result = { _, _, _, _, _ -> + Result.success(Unit) + } + + override suspend fun editMessage( + originalEventId: EventId?, + transactionId: TransactionId?, + body: String, + htmlBody: String?, + mentions: List, + ): Result = editMessageLambda( + originalEventId, + transactionId, + body, + htmlBody, + mentions + ) + + var enterSpecialModeLambda: (eventId: EventId?) -> Result = { + Result.success(Unit) + } + + override suspend fun enterSpecialMode(eventId: EventId?): Result = enterSpecialModeLambda(eventId) + + var replyMessageLambda: ( + eventId: EventId, + body: String, + htmlBody: String?, + mentions: List, + ) -> Result = { _, _, _, _ -> + Result.success(Unit) + } + + override suspend fun replyMessage( + eventId: EventId, + body: String, + htmlBody: String?, + mentions: List, + ): Result = replyMessageLambda( + eventId, + body, + htmlBody, + mentions + ) + + var sendImageLambda: ( + file: File, + thumbnailFile: File?, + imageInfo: ImageInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ) -> Result = { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + + override suspend fun sendImage( + file: File, + thumbnailFile: File?, + imageInfo: ImageInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ): Result = sendImageLambda( + file, + thumbnailFile, + imageInfo, + body, + formattedBody, + progressCallback + ) + + var sendVideoLambda: ( + file: File, + thumbnailFile: File?, + videoInfo: VideoInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ) -> Result = { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + + override suspend fun sendVideo( + file: File, + thumbnailFile: File?, + videoInfo: VideoInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ): Result = sendVideoLambda( + file, + thumbnailFile, + videoInfo, + body, + formattedBody, + progressCallback + ) + + var sendAudioLambda: ( + file: File, + audioInfo: AudioInfo, + progressCallback: ProgressCallback?, + ) -> Result = { _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + + override suspend fun sendAudio( + file: File, + audioInfo: AudioInfo, + progressCallback: ProgressCallback?, + ): Result = sendAudioLambda( + file, + audioInfo, + progressCallback + ) + + var sendFileLambda: ( + file: File, + fileInfo: FileInfo, + progressCallback: ProgressCallback?, + ) -> Result = { _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + + override suspend fun sendFile( + file: File, + fileInfo: FileInfo, + progressCallback: ProgressCallback?, + ): Result = sendFileLambda( + file, + fileInfo, + progressCallback + ) + + var toggleReactionLambda: (emoji: String, eventId: EventId) -> Result = { _, _ -> Result.success(Unit) } + override suspend fun toggleReaction(emoji: String, eventId: EventId): Result = toggleReactionLambda(emoji, eventId) + + var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> Result.success(Unit) } + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = forwardEventLambda(eventId, roomIds) + + var retrySendMessageLambda: (transactionId: TransactionId) -> Result = { Result.success(Unit) } + override suspend fun retrySendMessage(transactionId: TransactionId): Result = retrySendMessageLambda(transactionId) + + var cancelSendLambda: (transactionId: TransactionId) -> Result = { Result.success(Unit) } + override suspend fun cancelSend(transactionId: TransactionId): Result = cancelSendLambda(transactionId) + + var sendLocationLambda: ( + body: String, + geoUri: String, + description: String?, + zoomLevel: Int?, + assetType: AssetType?, + ) -> Result = { _, _, _, _, _ -> + Result.success(Unit) + } + + override suspend fun sendLocation( + body: String, + geoUri: String, + description: String?, + zoomLevel: Int?, + assetType: AssetType?, + ): Result = sendLocationLambda( + body, + geoUri, + description, + zoomLevel, + assetType + ) + + var createPollLambda: ( + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ) -> Result = { _, _, _, _ -> + Result.success(Unit) + } + + override suspend fun createPoll( + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result = createPollLambda( + question, + answers, + maxSelections, + pollKind + ) + + var editPollLambda: ( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ) -> Result = { _, _, _, _, _ -> + Result.success(Unit) + } + + override suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result = editPollLambda( + pollStartId, + question, + answers, + maxSelections, + pollKind + ) + + var sendPollResponseLambda: ( + pollStartId: EventId, + answers: List, + ) -> Result = { _, _ -> + Result.success(Unit) + } + + override suspend fun sendPollResponse( + pollStartId: EventId, + answers: List, + ): Result = sendPollResponseLambda(pollStartId, answers) + + var endPollLambda: ( + pollStartId: EventId, + text: String, + ) -> Result = { _, _ -> + Result.success(Unit) + } + + override suspend fun endPoll( + pollStartId: EventId, + text: String, + ): Result = endPollLambda(pollStartId, text) + + var sendVoiceMessageLambda: ( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + ) -> Result = { _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + ): Result = sendVoiceMessageLambda( + file, + audioInfo, + waveform, + progressCallback + ) + + var sendReadReceiptLambda: ( + eventId: EventId, + receiptType: ReceiptType, + ) -> Result = { _, _ -> + Result.success(Unit) + } + + override suspend fun sendReadReceipt( + eventId: EventId, + receiptType: ReceiptType, + ): Result = sendReadReceiptLambda(eventId, receiptType) + + var paginateLambda: (direction: Timeline.PaginationDirection) -> Result = { + Result.success(false) + } + + override suspend fun paginate(direction: Timeline.PaginationDirection): Result = paginateLambda(direction) + + override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow { + return when (direction) { + Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus + Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus + } + } + + var closeCounter = 0 + private set + + override fun close() { + closeCounter++ + } + + override fun toString() = "FakeTimeline: $name" +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/LiveTimelineProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/LiveTimelineProvider.kt new file mode 100644 index 0000000000..a62aeda6c6 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/LiveTimelineProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 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.libraries.matrix.test.timeline + +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.TimelineProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class LiveTimelineProvider( + private val room: MatrixRoom, +) : TimelineProvider { + override fun activeTimelineFlow(): StateFlow = MutableStateFlow(room.liveTimeline) +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 3e91494dcb..b96adefb6e 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -49,6 +49,7 @@ import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager import io.element.android.services.analytics.noop.NoopAnalyticsService @@ -145,7 +146,7 @@ class RoomListScreen( Singleton.appScope.launch { withContext(coroutineDispatchers.io) { matrixClient.getRoom(roomId)!!.use { room -> - room.timeline.paginateBackwards(20, 50) + room.liveTimeline.paginate(Timeline.PaginationDirection.BACKWARDS) } } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt index b7beaaa5e9..4e49560398 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt @@ -71,6 +71,13 @@ inline fun lambdaRec return LambdaFourParamsRecorder(ensureNeverCalled, block) } +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2, T3, T4, T5) -> R +): LambdaFiveParamsRecorder { + return LambdaFiveParamsRecorder(ensureNeverCalled, block) +} + class LambdaNoParamRecorder(ensureNeverCalled: Boolean, val block: () -> R) : LambdaRecorder(ensureNeverCalled), () -> R { override fun invoke(): R { onInvoke() @@ -109,3 +116,12 @@ class LambdaFourParamsRecorder(ensureNeverCal return block(p1, p2, p3, p4) } } + +class LambdaFiveParamsRecorder(ensureNeverCalled: Boolean, val block: (T1, T2, T3, T4, T5) -> R) : LambdaRecorder( + ensureNeverCalled +), (T1, T2, T3, T4, T5) -> R { + override fun invoke(p1: T1, p2: T2, p3: T3, p4: T4, p5: T5): R { + onInvoke(p1, p2, p3, p4, p5) + return block(p1, p2, p3, p4, p5) + } +} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Day-33_33_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Day-34_34_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Day-33_33_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Day-34_34_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Night-33_34_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Night-34_35_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Night-33_34_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Night-34_35_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Day-34_34_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Day-35_35_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Day-34_34_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Day-35_35_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Night-34_35_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Night-35_36_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Night-34_35_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Night-35_36_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Day-53_53_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Day-54_54_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Day-53_53_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Day-54_54_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Night-53_54_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Night-54_55_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Night-53_54_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Night-54_55_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Day-39_39_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Day-40_40_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Day-39_39_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Day-40_40_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Night-39_40_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Night-40_41_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Night-39_40_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Night-40_41_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-35_35_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-35_35_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-35_35_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-35_35_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-35_35_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-35_35_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-35_36_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-35_36_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-35_36_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-35_36_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-35_36_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-35_36_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-36_36_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-37_37_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-36_36_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-37_37_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-36_37_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-37_38_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-36_37_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-37_38_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-37_37_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-37_37_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-37_37_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-37_37_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-37_37_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-37_37_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-37_38_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-37_38_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-37_38_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-37_38_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-37_38_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-37_38_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-38_38_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-38_38_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-38_38_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-38_38_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-38_38_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-38_38_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-38_39_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-38_39_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-38_39_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-38_39_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-38_39_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-38_39_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-40_40_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-41_41_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-40_40_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-41_41_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-40_41_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-41_42_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-40_41_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-41_42_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Day-41_41_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Day-42_42_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Day-41_41_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Day-42_42_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Night-41_42_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Night-42_43_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Night-41_42_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Night-42_43_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-42_42_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-43_43_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-42_42_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-43_43_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-42_42_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-43_43_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-42_42_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-43_43_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-42_43_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-43_44_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-42_43_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-43_44_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-42_43_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-43_44_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-42_43_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-43_44_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-43_43_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-43_43_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-43_43_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-43_43_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-43_43_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-43_43_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-43_43_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-43_43_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-43_44_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-43_44_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-43_44_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-43_44_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-43_44_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-43_44_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-43_44_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-43_44_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-44_44_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-45_45_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-44_44_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-45_45_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-44_45_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-45_46_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-44_45_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-45_46_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Day-45_45_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Day-46_46_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Day-45_45_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Day-46_46_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Night-45_46_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Night-46_47_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Night-45_46_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Night-46_47_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-46_46_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-46_46_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-46_46_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-46_46_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-46_46_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-46_46_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-46_47_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-46_47_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-46_47_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-46_47_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-46_47_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-46_47_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-47_47_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-47_48_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-48_48_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-49_49_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-48_48_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-49_49_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-48_49_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-49_50_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-48_49_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-49_50_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-49_49_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-49_49_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-49_49_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-49_49_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-49_49_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-49_49_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-49_50_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-49_50_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-49_50_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-49_50_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-49_50_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-49_50_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-52_52_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-53_53_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-52_52_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-53_53_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-52_53_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-53_54_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-52_53_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-53_54_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_12,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_13,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_14,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-51_51_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_12,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_13,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_14,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-51_52_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Day-50_50_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Day-51_51_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Day-50_50_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Day-51_51_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Night-50_51_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Night-51_52_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Night-50_51_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Night-51_52_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-54_54_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-55_55_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-54_54_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-55_55_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-54_55_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-55_56_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-54_55_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-55_56_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Day-55_55_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Day-56_56_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Day-55_55_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Day-56_56_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Night-55_56_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Night-56_57_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Night-55_56_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Night-56_57_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-57_57_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-57_58_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-56_56_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-56_57_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-58_58_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-59_59_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-58_58_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-59_59_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-58_58_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-59_59_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-58_58_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-59_59_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-58_59_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-59_60_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-58_59_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-59_60_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-58_59_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-59_60_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-58_59_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-59_60_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-59_59_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-60_60_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-59_59_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-60_60_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-59_60_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-60_61_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-59_60_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-60_61_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-60_60_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-61_61_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-60_60_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-61_61_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-60_60_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-61_61_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-60_60_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-61_61_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-60_61_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-61_62_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-60_61_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-61_62_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-60_61_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-61_62_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-60_61_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-61_62_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-61_61_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-62_62_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-61_61_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-62_62_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-61_62_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-62_63_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-61_62_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-62_63_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-62_62_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-63_63_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-62_62_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-63_63_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-62_63_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-63_64_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-62_63_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-63_64_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-63_63_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-63_63_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 9b3c889d05..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-63_63_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c464ff6c892364dc096a8edba88074d891b9baa91f4ba50197c7a500d76c0fa7 -size 6247 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-64_64_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-64_64_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7bc7202be5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-64_64_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74cc9020e14baa41c05e07f2a804746df595d42c8343e139f05b57c508e1b59c +size 6298 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-63_64_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-63_64_null,NEXUS_5,1.0,en].png deleted file mode 100644 index e443bf7f5d..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-63_64_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03e53795df5b0078a7883a4ed181e2a022aeec9f095eaddeed213e3de9b16069 -size 6200 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-64_65_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-64_65_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ea1240cd75 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-64_65_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:492c2a10c3d643d24a2d578baf385a7019a86b39bf14f3d077b3233a8d7524a5 +size 6217 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Day-24_24_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Day-24_24_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c03e811149 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Day-24_24_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0a170270406e7ba3f4d21d67536dd2866a091fd81c9474897143de98dfe6df9 +size 135600 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Day-24_24_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Day-24_24_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ab573da955 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Day-24_24_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e465ef114b738f0edbd6094cab6bb6b9704d9de9f7825e62cd0792d46d30bae +size 147050 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Night-24_25_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Night-24_25_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bb72dcbe78 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Night-24_25_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15d9542712c4799955d2c673ceb2e927c2a24b86ac092fb98ce02bcc1bb216f3 +size 134873 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Night-24_25_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Night-24_25_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9a6986b00c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_null_TimelineItemEventRowWithReplyOther-Night-24_25_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fcf8f3cb71494b249a53f264bc3281de853da2dff07479c065c1a41bf2aff63 +size 146032 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-24_24_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-24_25_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Day-26_26_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Day-27_27_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Day-26_26_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Day-27_27_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Night-26_27_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Night-27_28_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Night-26_27_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Night-27_28_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-25_25_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-25_25_null,NEXUS_5,1.0,en].png deleted file mode 100644 index d6cfb16784..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-25_25_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:35bf09d08616a2b3867608ad948bbf3442b2c8a2560116ae36b01673cbbbf6f7 -size 14568 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-26_26_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-26_26_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0e898bf24c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-26_26_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:031b16204426f762489bbf32d1667b9dc504fdc8c86535cad8ac8cca646c2564 +size 24135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-25_26_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-25_26_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 94f097ddaf..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-25_26_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d662de6189258cb24f1c54fdb14ceb8e6a53620ce54643709999e5e7a9a184f -size 14603 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-26_27_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-26_27_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4b10887481 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-26_27_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:006894de328a6ec6a0d3ebac67145d695a4be790fbda9a39e932363a9f541d0e +size 23230 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-27_27_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-28_28_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-27_27_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-28_28_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-27_28_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-28_29_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-27_28_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-28_29_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-29_29_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-30_30_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-29_29_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-30_30_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-29_30_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-30_31_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-29_30_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-30_31_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-30_30_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-31_31_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-30_30_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-31_31_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-30_31_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-31_32_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-30_31_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-31_32_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-31_31_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-32_32_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-31_31_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-32_32_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-31_32_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-32_33_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-31_32_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-32_33_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-28_28_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-29_29_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-28_28_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-29_29_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-28_29_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-29_30_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-28_29_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-29_30_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-32_32_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-33_33_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-32_32_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-33_33_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-32_33_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-33_34_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-32_33_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-33_34_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-64_64_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-65_65_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-64_64_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-65_65_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-64_65_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-65_66_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-64_65_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-65_66_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4e44ed5288 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf70dc030f96b929ba3817694a624de715b82719117fc75720c4e3ff8a8188c8 +size 9962 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f33e3346a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcf0155249cd3dd0851e0202f732f281fad30a1931e57077c44fdf19b2b90f44 +size 13680 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f33e3346a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcf0155249cd3dd0851e0202f732f281fad30a1931e57077c44fdf19b2b90f44 +size 13680 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6a83c83c38 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5315224059776b19247788a9902939829733c5dd520cdfb95e6444509a4413ae +size 13932 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..825f2f46d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8b6b2f2e2c03e7927ccb93d4fa8e914f7a3a19f4526cda8237e324dda3d9d3f +size 7548 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc56b41fc3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d5d4105517ea18a1e710be7ad727e096d641c07a29f4c72d3aac43d0330b2f3 +size 10957 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc56b41fc3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d5d4105517ea18a1e710be7ad727e096d641c07a29f4c72d3aac43d0330b2f3 +size 10957 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..375ed72288 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9da3d0066a9f12a0826a20f760c9fddc5f58b1614406eff321a7fce1325803d +size 11165 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_0,NEXUS_5,1.0,en].png index d9a943df0c..7653bda8ff 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3346e3e5a48f51fa2dd92cfc41663ce8aab46ad2c7a6ddf03e716488c23a3eb7 -size 52900 +oid sha256:01f357adba4548473fe877a07b011ceaa5a178f99ac900dea43d01d60c1a0f94 +size 57952 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_1,NEXUS_5,1.0,en].png index c993331871..3f7240f6e9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3adfe3cc79c1a55d6323644507a18fa893033adb2a243b444c7140188bd6e4cd -size 74734 +oid sha256:068c1af330928fd29430ff1ae19191c301bc47ca7287c44a313f591a0e939ce8 +size 77949 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_10,NEXUS_5,1.0,en].png index a4fb23768a..02ab9c1c61 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:508dd6992bd02c65c18e636376303af3d88d3c98b2b4baf5c7ba45b0f4116594 -size 347900 +oid sha256:e8b50e3c122c296eb28dd4bb1a35efa1c41058dac73c2bb3ed335b399a2688ce +size 355047 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_11,NEXUS_5,1.0,en].png index b057e2366d..c6115adb71 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19daf8b409eda4b1c70ec0822703fc75734dedaf15405632bd015bd8b9361cc7 -size 80256 +oid sha256:dd99189b5eae40c9c76c8f026d6594dd8b5014b4e19fd2114e282b01ddbd90bf +size 96283 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_12,NEXUS_5,1.0,en].png index fb11a3aa0c..4bff5f0b50 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d566fde31c28cb1aea24ab9203969a1dffeeca82cb42dc43f09b85f838f0e27 -size 54399 +oid sha256:35b5000c83642d92d179acee804e15977d2f77680a312f8f76b84bf6f93a652d +size 59357 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_13,NEXUS_5,1.0,en].png index b52818eb20..5246070904 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec4c766a59655ef307230b3da8c7000c115df97a5b0fe3eab8460f721215182f -size 66881 +oid sha256:63e8554b77aaba345cdf8d636b6983b010553e3734f3ce057e1f98682c371d46 +size 70692 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_14,NEXUS_5,1.0,en].png index 47560bfed5..32a058575c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e4c783d17349b9a8280a739f16d1fe493794c85df7edcae64126e59130ce7de -size 50673 +oid sha256:a2f8fa9960347f2e728d9817c7622e21207dc5eb21ea167da17593833633a763 +size 56094 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_15,NEXUS_5,1.0,en].png index 8f5d4499dd..e0dc47affa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_15,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_15,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2384468708808ff65f38add76d0b0b30c3a737c069021989a904e859aa7ce762 -size 67772 +oid sha256:9aa8bf2c707800a5bef7f3487d359fec33ef3f51529072a00e79a2ab55595337 +size 71602 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_16,NEXUS_5,1.0,en].png index 88013bff3e..35097d1d97 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_16,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_16,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00254155626cb17b13012f39e02bd91d844ee8f8e66f2eb46808e9d0559a2e58 -size 58094 +oid sha256:d4fe5c5d4c91369a2e668c4bcd0ce966ce03935d11dc282cefd27c0f35ce791c +size 62484 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_2,NEXUS_5,1.0,en].png index e8cb515769..53ce3b677f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f12db44cac1283813617671f12d7841075267fdfdd637248d8ca3b873db0574 -size 198971 +oid sha256:47c0668cf7c6b04dd136723798344c7a391deb83063c39d9d7a267b264be757b +size 223068 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_3,NEXUS_5,1.0,en].png index 19e44adc48..c979f1e764 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f59102f1b080437c58c9ea29a0c24710e1a77699cde64f025bdc984598728bf6 -size 199875 +oid sha256:e84eeec6485d20b7bcda16a9bc1a135feddac05cebc2b9b902eaccfd782ad12f +size 224298 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_4,NEXUS_5,1.0,en].png index 17e245bd6c..43b1d7d3e8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4051e35af17b29831c15a9ce2daab7873856d499352284cc266d9a16cf6ec570 -size 72657 +oid sha256:fd7c86942aad6beeee3c24e73247fd60479d7917bc04f4dff33da642f406afe3 +size 79505 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_5,NEXUS_5,1.0,en].png index c906f23282..9efecb296b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7184a3edb75e7de06a9f1a8c77bc5ee857c8bc6c1332d0fd669976e33f4a7095 -size 88512 +oid sha256:53c8c167256bf3aed82d96ccd87ce322ee9e1aadd68c75a5023e422d84d97403 +size 92700 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_6,NEXUS_5,1.0,en].png index 7146940ea8..36af7732d1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d7779ff91f299f19b77a77c7c5037f6602300fd7d5013602f454231f235ff99 -size 74927 +oid sha256:bc85cc740fc1e15e2680d3bd34e8d16d60e94f8c21391db48cc37620cf2fbb71 +size 81861 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_7,NEXUS_5,1.0,en].png index 92a76ff176..4844572917 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c87aa2140d49ff777e83e2c034eda87bf782dc2de2e3ad8dda30b683ea77491 -size 106818 +oid sha256:ef1aeb93f8b0ea48393af6c7b515ecf530c696dda3b05ad5b7b0f102a70ceb4a +size 110876 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_8,NEXUS_5,1.0,en].png index f53af6bbcd..a2d00d0658 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e70fc1e04cfaa3854b1951435c1b1181660fbe47bd3cf67c4cf58b88d3ce5cdc -size 56601 +oid sha256:9feaa51ab3dd20a3a0f69b491118b0cb0e44389b7b72926173d690780e3158f3 +size 61015 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_9,NEXUS_5,1.0,en].png index 59932af996..9f606059cb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-9_9_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cdcdc475a586e43da2ae557399c4b01d412f0ce74ef93a27e15cdd7b0bb679d -size 393373 +oid sha256:e8ba1ded44b0093b82d63903b6ce13c43f9fb187eec328bccb25b1c2761bb004 +size 398839 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_0,NEXUS_5,1.0,en].png index 313d5b49f7..9188618ad6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:450763462987ae38f8d3531a110982284c5dc0e5ed1e5c0900d84ff88e2237dd -size 51022 +oid sha256:fff88277974635316e9c48876794a45fc22605553ae1027c02f097d68b5dc460 +size 55709 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_1,NEXUS_5,1.0,en].png index 12f4ded419..1905ff6055 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:836eb753146dd70407a64b9c918c85245c753bebc448a275f9da9ff7d0a78d8e -size 71645 +oid sha256:02af125ead62964cd3d05c509d8b02a4ca1c2c3d9be449daaa3365bf0288bc88 +size 74665 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_10,NEXUS_5,1.0,en].png index 765f26f6cc..e13dd59ad5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f300e31cc6ade1665b134604682a43f9023bf2018ad58dca6b875da83440b2bf -size 178263 +oid sha256:823d32bb40bb091f645a3d73db1480970a7c65120e009e2922d1d29564bf82b5 +size 186340 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_11,NEXUS_5,1.0,en].png index a0764bcda6..d9daeec206 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:923a8664899986f9c9af1d533af9e1bf55d446a75b31694fc11aea1ff05b2bff -size 79334 +oid sha256:de43fb6408ac31359061bf6374d377798f481e6478862a307d73b42713ef70b6 +size 95613 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_12,NEXUS_5,1.0,en].png index fa96586263..8003345444 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:725b8dfcc5e5ac4a849b5e8408bf0796c96566bdb7460b0908b6c8da8731850e -size 52680 +oid sha256:c3083c45f084526ba69a52b5710f9a224699c871f78cd7daa033152c05cb9774 +size 57171 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_13,NEXUS_5,1.0,en].png index 02ce21f21c..ca4f60522b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99541143d6e6337312800ce96d799dbe4a45b615a52b28685b0aef2e30a35502 -size 64205 +oid sha256:164313ceaebd85020ba7322e5d3fd6fd692975270cc3f375bc6594a9f38cb877 +size 67884 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_14,NEXUS_5,1.0,en].png index d8c7c5d9ba..84f8e8f64f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d6579010f071d931ab155377ed8691af5cb964235f1d50703691d5014e22528 -size 48954 +oid sha256:ab1e4101e3dab6f2d26e6385ea2d044853edfcf7dd54f2e2bd6a41b686ec7962 +size 53751 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_15,NEXUS_5,1.0,en].png index 155602b37c..00e0f2a236 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_15,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_15,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb2d30e7a698b38c7f80384e532e9903ebb1bd793d678a557deff4789cf36e9c -size 64956 +oid sha256:30fad008f20cad13d2a688c17d98f27e677c952d8227b7f8d69bf5f9034fba26 +size 68795 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_16,NEXUS_5,1.0,en].png index 2c83ffe8ec..7a5868deac 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_16,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_16,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58ae49eed2bfe851a4c368973ae285f0d7d04665154f2ab131518948980ab0d2 -size 55891 +oid sha256:58cd9f6a2534f962d16c120045d243cfee36cfcb58362809a3ce97d01dc23568 +size 59811 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_2,NEXUS_5,1.0,en].png index eb0205e5cd..182e39e88e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7261eb43b9d9847f405574b984d385365f5ba767c23837d7afc19077b4f6069c -size 198834 +oid sha256:48b4b6d0e78fca35b674c33440316ad7c440c4abb64ed861150295180bbc2a00 +size 222325 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_3,NEXUS_5,1.0,en].png index 1ca34f82f6..0e49e57597 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d02b0fa3e17aa3388bf280135762e890bd210cf032baf3c96a93940c9dfbd23 -size 199794 +oid sha256:deae4682afadb57a1e2950cc4304005eabf380e08926f7914db0c27bbb82ef38 +size 223556 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_4,NEXUS_5,1.0,en].png index 43ee0cf46e..8ad33da28f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fa23f91ee527428180bc08b66ab1e140255baf60cc541f87b378eaccae41eb0 -size 69721 +oid sha256:e2e7bc8a750fd6d4311ad5b9ce74eb20edebf90d44e0b4f7c830b098c55f1fa4 +size 76105 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_5,NEXUS_5,1.0,en].png index 7e4d6daddb..c0d758ae8a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b6f55420804e82244e00ef14c178ebba1f353425a56428428f8cc028fb85370 -size 83978 +oid sha256:57ab836a605bcab4d4bdf7eac25692d7f876ecd4e8ff80c0f47105d6fc020879 +size 88013 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_6,NEXUS_5,1.0,en].png index 97c5cc8bf2..4030bb2c56 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0b5f77065d71a319a3981a3ee245f91fd8926710051b146a97bab7ccef9aca1 -size 72201 +oid sha256:dbc583db87432412befcc64b4930faf30419c796b7aff62986cef5593bb1817a +size 78791 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_7,NEXUS_5,1.0,en].png index 6fee67edad..ebb95c693d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b13774dfc0adcc300f1dbd71be3a26fae6b2a0b8bef1e83c114daa268f388079 -size 100885 +oid sha256:355a6baddbf772ad85a94a20ddd6e92e43526f8c166bb09a50ef43aab35d14ad +size 104693 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_8,NEXUS_5,1.0,en].png index 6badd2e5e5..5af3ec352b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a004d83e36d52b2d113f9475e392cee55649589e6e67c8efc361cac88b130d50 -size 55201 +oid sha256:3053ab49659fb8bb5604772e32dca057a7d3c8ad967272bfd899c35207b73f0f +size 59756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_9,NEXUS_5,1.0,en].png index b9a4d15b9d..177c199abd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-9_10_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:daa2eafe0a840ef99aff1020dbed2b0e31fedee604b05cc3e6642af7c3e968e5 -size 189474 +oid sha256:acdc972a8a61dacb2387f5131e044ab872038c246c45241c6b464c6ed2b3d244 +size 194776 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index c2df9c0ca7..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aa33a2dc4094680f3aa5b5e98efec6c02d585d98ff94ba6351ba7589fa156438 -size 55386 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index a68d5922a0..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8ba24739d9425c00794696d8a94b6261e605264da090a2c2702d5a81a67bfb46 -size 56180 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index a0cc74f6c6..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7bd915ebfc3de3703fc039a02064c03dce3013580fd25f47582b0b7612785b9 -size 52110 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f0789db8fe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34a3724a2c47e61e6c50848a40a8a03271f3c33cedbdf65bca39541084cf5d41 +size 64908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c14bf5d943 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edbd8b2aedaf59a54dd6cbc4617a0cf962a5b215d8412d857f46167ab5584e7f +size 65737 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e61c636936 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98b18653d6ec87c745e3341b6eb9d8403bf1e7488dcdaec1495df0c8ab1e2861 +size 61630 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index dcf2410f8e..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22b316a3e0c0f9804578ac1d8e038c1087d2f2f3d71244a53f254a1d35d3512e -size 54131 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 3b9d2f642b..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c6b4053d9e86e963919d6462849cf0bfbaf9df7f14330e64046d9d3cb1f5fbdd -size 55030 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index a1eaf95736..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:daed63aa46f1d4e16de8b554616312b6a0d370768971894da346724ec9fdc145 -size 51012 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..111a343f3f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0351ee6b982b1894884dfd4e116b836a033d7496060f467ed678dc991a630686 +size 63946 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..42d3042545 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1fbe0db81c73883ffda9eac3964a768b3bcb7bdbf0a694a6a0f628a47c3abbc +size 64863 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5c71ee6478 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27167a8130d1ab01432d0f7a58d51328a1a0361943b5b0d7d258e42e09afafd9 +size 60802 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-66_66_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-66_67_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png index da5e3fcdf0..06062283f6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59d50da7714bca7f9c1dc84216200e6063c02e1ea4337234fee49490730accb6 -size 57051 +oid sha256:eddc6d3e3df386d98db6604c863c7d16b29f55b6c66e5bcd108adca85d5ff3cc +size 66439 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png index 89a6695ea1..2a8b2e16ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8e0026f5d91eecda82aba901adad7cbef701fc826de759ca9f46291418f75f9 -size 55935 +oid sha256:c8868918a3bf66bbef19d5c3db6fc3142749a6e0ceaea137af98b984b381c02c +size 65909 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png index b68eef26e9..299923153f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f88fbe042736ea55c2b70d980c9cf40edad6560d23220ab3317c4584f8825ef -size 58724 +oid sha256:09c436daccf75627f566973dc90cbfdfb1133e2d9dfe48b7ed2803aed8cd6113 +size 68078 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png index f4c2b33509..872c92764a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34ab1f97b8d2adc58f2d86c43557b2fcd726532cd051366c4bd79b4e775b37ae -size 52808 +oid sha256:eed1aa062d7bd5ab65d9526f82d5544e30415d2a628ef000a947a189e431e406 +size 55593 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_12,NEXUS_5,1.0,en].png index 91a814763d..1a5b888264 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7aa093b0a7d678fb1324946fd5463fcd1206c602080b12bf614f2d48fcbf6742 -size 57069 +oid sha256:712bb9fef80d03d3fba835553707fc7a885a5bd175abfbac929da732e3aa914c +size 66453 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png index 5195c06123..89beb32409 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c92aeae7764707d6a39cf46f64304cb3a300eea5126779d8c4be604eb73c259c -size 58914 +oid sha256:8a15992f14ecd677239988c05e13c6a1d9e301fd0a9d9ee15ea4963c9cbfa3ba +size 68781 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png index 42945f4e1f..cf71e1aacc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b15640f4d75e9fd03a951500bc5ac951074a50f1bd509b777b6248e7883901e7 -size 57599 +oid sha256:061c02fa1f32f25f37227446469e9460e0fe0593be54dee632b3e217bc783572 +size 60780 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png index 53e2d0267a..037379589c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06aa006c18c983bd9a8af378d72620ef545d7799bf9b945f53c68877fdeb41b7 -size 54847 +oid sha256:18623230b671c8cb4e9e1561b4a12ea607d8961525387d4120d0d695a0c9a057 +size 64198 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png index c51b9ba8c4..818021066a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01b31acd0f223cf78ee14cb0197204d49ee23a008ab428d974b8852f17c1847c -size 54696 +oid sha256:db37cab9354ee4c86039799665f71eb0ce35b69effc429dbc8af104a25e76d88 +size 64447 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png index 72738a1353..564731ce97 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20b2f2954e2200577d8f8b5be6125e3f75f271c0f61626ac90344a139488f9f6 -size 61845 +oid sha256:80ce7935681b2f82d45f98dcdc11dd171cb4a71db842a1f2a08ff1ddd64e45ea +size 64160 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png index 0bcfd1f274..51516ac05f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50a033f6a3637e808a664cdee2cfa0bc9c2df8c91ec6d47b4eb30f87b5ca2e8c -size 42957 +oid sha256:aa1513a29201bff2f7c829ccafdc008818528a2a2c076b4d51405854e965b899 +size 45335 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png index 880cf820cf..db6219b31e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc634e8024c6a02c58f805c63ff7511f76ed1ab8291215801936b69d299ba71d -size 42117 +oid sha256:4359139aec1216d2fef31842037bea926743ef2df3f319828987fcfd4e3fee30 +size 44525 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png index f71061cfb9..fbf20a84e9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dba29739541d349cc688274f9a36add6d29541af88161b46a26fa94dc04a550f -size 55102 +oid sha256:b13ba3f24be0236a57cdf772beab06ca0b58f4549a4714ec4fb80667597cecbb +size 64293 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png index 02a68cbe05..f2812a1fe5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3cc9539d303996fea60fe4fd955598ec614d0a10be0f14727590d68291c66aa -size 53971 +oid sha256:d28b7d12ad741c7eaf2a08baa4f0547ef5c2aefd40688c9bf3019c8e0cdc40b0 +size 63673 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png index cffabbd3b4..bfa7db723b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:baba3207129e98cc3d0c56575e6a02a213f9a980be9c7ede4f053ff3bd7d24c5 -size 56530 +oid sha256:2bb1a5b2889800c3accb6390a7a01c3f4d11cae75da88c012988f6415836d496 +size 65801 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png index d254cc77b8..4179df43c7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:961c098272b918099b85e4395ce8950bdbdce9eda9771741fe92d5c24433bc8c -size 47318 +oid sha256:f0cd86fce4381e8ed9d3095020aa0d175221d0ac864a5f339d363f9dd150b7e1 +size 48933 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_12,NEXUS_5,1.0,en].png index 446b41706b..d4121e20ba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9d6c000c1b200698152d0ade87efb8e9779ea6ec7524ec382a4e1760d9cbf7a -size 55118 +oid sha256:285c7b31233af00920a8627e3e6a0b30c3916380d2b017b7d2106975725e7122 +size 64318 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png index de7de02a49..d35d23f452 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84298b824ecab7cb113bde4981f47ede93739cb98152d6b97b2536e3b8881329 -size 57172 +oid sha256:f0b7f07e311cd791afa841e115f3a39966e3866b8808545b7e6b4cb78fa08513 +size 66410 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png index f8fb58ac73..ce6f3d82da 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b30d4d37535cfdc79dee0a4bcd2fd313b20a701b00dbab0ed008822b3b7c80ea -size 52440 +oid sha256:644385471358d217b7eee5043419e3885cec12b9ee3b7220a723ab718184af43 +size 54253 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png index dab4c6b633..11508836df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b31f3772ccc37782d6a20e3b10a080fa7656fb8a24c9ddf46873e8c272b448b1 -size 52730 +oid sha256:1b6d26e85d7dc6e4383b63e008ac0c7010e277ae9d9701367ff67b48a0b8d32d +size 61874 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png index cc08f4ac26..687dae04d3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96016c441fd3f8a3283d8c4eee088a40502df1e41f6181ea6d488f67dad6d5a1 -size 52726 +oid sha256:898cc4eacaf946b7fe3f80c1eef228c7efd878bf47bceb9dda36dcb132dcc556 +size 62371 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png index b250014c1e..58d1d2d20c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55e7c770dc276c81a98819648ba371ac5c17b681a1e0a82e2699d391429efefd -size 56462 +oid sha256:0902a2d1c4bcd56b54fdfba543ca2d4b5bc52ece9491f24edaf85b4f2845868e +size 57742 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png index d583fc2f29..f9eb25db65 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83587419132d5d59596f85b1b4f6213a1bba7576e35ff31933725b38abd3bcaf -size 39609 +oid sha256:eb95c69e054b36dac81293c9c67fe6a249ab5cd2a146f1ef7d316cba858e0a0a +size 40666 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png index 8016c26902..c64c93756a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef5c1a45982605a4f3c253dfbc280c24acdd84ea9446d3c9709823e10cdf6c05 -size 38817 +oid sha256:f513e9430fe5447ccfd162ff2693889e1d332434186da6d494c91a4b7f5d460b +size 39890 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_ColorAliases_null_ColorAliases-Day_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_ColorAliases_null_ColorAliases-Day_0_null,NEXUS_5,1.0,en].png index b090053fa3..e966144767 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_ColorAliases_null_ColorAliases-Day_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_ColorAliases_null_ColorAliases-Day_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:096dd17663bcacc9349cd9f31fafff1b76f35bd8d9a7a08d080d8b858b7f3e1d -size 62792 +oid sha256:4f2148c471455c65f6745b3972c2b7241d1286188c91842bf253d915c7aea6b3 +size 74511 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_ColorAliases_null_ColorAliases-Night_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_ColorAliases_null_ColorAliases-Night_1_null,NEXUS_5,1.0,en].png index 01d2e8ac5a..943e60e8e6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_ColorAliases_null_ColorAliases-Night_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_ColorAliases_null_ColorAliases-Night_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc08b47e2f2627a751931958e4fd017a24793df71211bbcb7f2ee6d7a9ae1a83 -size 62530 +oid sha256:35c2946241c996d03d42585c95d3a9a0d8cd4ba17fad0d79102ab407618c3951 +size 73952