From 66e25158793eb2519d1ed0350f4614267d264b94 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 16 Apr 2024 21:18:54 +0200 Subject: [PATCH 01/53] Timeline : expose new pagination status api --- .../impl/timeline/RoomTimelineExtensions.kt | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) 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 f8109259d5..400ff8fffd 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 @@ -27,13 +27,14 @@ 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.BackPaginationStatusListener +import org.matrix.rustcomponents.sdk.PaginationStatusListener +import org.matrix.rustcomponents.sdk.TaskHandle 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.BackPaginationStatus +import uniffi.matrix_sdk_ui.PaginationStatus internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List) -> Unit): Flow> = callbackFlow { @@ -58,17 +59,28 @@ internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List = - mxCallbackFlow { - val listener = object : BackPaginationStatusListener { - override fun onUpdate(status: BackPaginationStatus) { +internal fun Timeline.backPaginationStatusFlow(): Flow = + paginationStatusFlow { listener -> + subscribeToBackPaginationStatus(listener) + } + +internal fun Timeline.forwardPaginationStatusFlow(): Flow = + paginationStatusFlow { listener -> + subscribeToForwardPaginationStatus(listener) + } + +private fun paginationStatusFlow(subscriber: suspend (PaginationStatusListener)->TaskHandle): Flow{ + return mxCallbackFlow { + val listener = object : PaginationStatusListener { + override fun onUpdate(status: PaginationStatus) { trySendBlocking(status) } } tryOrNull { - subscribeToBackPaginationStatus(listener) + subscriber(listener) } }.buffer(Channel.UNLIMITED) +} internal suspend fun Timeline.runWithTimelineListenerRegistered(action: suspend () -> Unit) { val result = addListener(NoOpTimelineListener) From e4174f279245305ddff379e28b675e3c6e8c88ee Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 Apr 2024 21:32:02 +0200 Subject: [PATCH 02/53] Timeline : start reworking timeline apis --- .../features/messages/impl/MessagesView.kt | 9 +- .../messages/impl/timeline/TimelineEvents.kt | 3 +- .../impl/timeline/TimelinePresenter.kt | 20 +- .../messages/impl/timeline/TimelineState.kt | 5 +- .../impl/timeline/TimelineStateProvider.kt | 25 +- .../messages/impl/timeline/TimelineView.kt | 26 +-- .../timeline/components/TimelineItemRow.kt | 2 + .../components/TimelineItemVirtualRow.kt | 16 ++ .../virtual/TimelineItemVirtualFactory.kt | 7 + .../TimelineItemLoadingIndicatorModel.kt | 24 ++ .../virtual/TimelineItemRoomBeginningModel.kt | 21 ++ .../impl/timeline/TimelinePresenterTest.kt | 13 +- .../impl/timeline/TimelineViewTest.kt | 2 - .../features/poll/impl/data/PollRepository.kt | 2 +- .../poll/impl/history/PollHistoryPresenter.kt | 16 +- .../libraries/matrix/api/room/MatrixRoom.kt | 4 +- .../matrix/api/timeline/DetachedTimeline.kt | 24 ++ .../matrix/api/timeline/LiveTimeline.kt | 25 ++ .../matrix/api/timeline/MatrixTimeline.kt | 48 ---- .../libraries/matrix/api/timeline/Timeline.kt | 34 +++ .../item/virtual/VirtualTimelineItem.kt | 7 + .../matrix/impl/room/RustMatrixRoom.kt | 24 +- .../impl/timeline/AsyncMatrixTimeline.kt | 106 --------- .../impl/timeline/RustDetachedTimeline.kt | 20 ++ .../matrix/impl/timeline/RustLiveTimeline.kt | 198 ++++++++++++++++ .../impl/timeline/RustMatrixTimeline.kt | 220 +----------------- .../matrix/impl/timeline/TimelineFlows.kt | 40 ++++ .../HasEncryptionHistoryBanner.kt | 26 +++ .../LoadingIndicatorsPostProcessor.kt | 44 ++++ ...essor.kt => RoomBeginningPostProcessor.kt} | 27 ++- .../DmBeginningTimelineProcessorTest.kt | 24 +- .../matrix/test/room/FakeMatrixRoom.kt | 1 - .../test/timeline/FakeMatrixTimeline.kt | 1 - 33 files changed, 593 insertions(+), 471 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingIndicatorModel.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemRoomBeginningModel.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/DetachedTimeline.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/LiveTimeline.kt delete mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt delete mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustDetachedTimeline.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustLiveTimeline.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/HasEncryptionHistoryBanner.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt rename libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/{DmBeginningTimelineProcessor.kt => RoomBeginningPostProcessor.kt} (73%) 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/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index cf02664a98..62ec074bea 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 @@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.timeline import io.element.android.libraries.matrix.api.core.EventId sealed interface TimelineEvents { - data object LoadMore : TimelineEvents data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents data class OnScrollFinished(val firstIndex: Int) : TimelineEvents @@ -28,6 +27,8 @@ sealed interface TimelineEvents { */ sealed interface EventFromTimelineItem : TimelineEvents + data class LoadMore(val backwards: Boolean) : EventFromTimelineItem + /** * Events coming from a poll item. */ 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..eede76dae9 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,9 +54,6 @@ 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 room: MatrixRoom, @@ -73,7 +70,7 @@ class TimelinePresenter @AssistedInject constructor( fun create(navigator: MessagesNavigator): TimelinePresenter } - private val timeline = room.timeline + private val timeline = room.liveTimeline @Composable override fun present(): TimelineState { @@ -85,7 +82,7 @@ class TimelinePresenter @AssistedInject constructor( val lastReadReceiptId = rememberSaveable { mutableStateOf(null) } val timelineItems by timelineItemsFactory.collectItemsAsState() - val paginationState by timeline.paginationState.collectAsState() + val paginationState by timeline.backPaginationStatus.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) @@ -99,7 +96,13 @@ class TimelinePresenter @AssistedInject constructor( fun handleEvents(event: TimelineEvents) { when (event) { - TimelineEvents.LoadMore -> localScope.paginateBackwards() + is TimelineEvents.LoadMore -> { + if(event.backwards) { + localScope.paginateBackwards() + }else{ + //TODO implement pagination forward + } + } is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId is TimelineEvents.OnScrollFinished -> { if (event.firstIndex == 0) { @@ -152,6 +155,7 @@ class TimelinePresenter @AssistedInject constructor( val timelineRoomInfo by remember { derivedStateOf { TimelineRoomInfo( + name = room.displayName, isDm = room.isDm, userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = userHasPermissionToSendReaction, @@ -161,7 +165,7 @@ class TimelinePresenter @AssistedInject constructor( return TimelineState( timelineRoomInfo = timelineRoomInfo, highlightedEventId = highlightedEventId.value, - paginationState = paginationState, + backPaginationStatus = paginationState, timelineItems = timelineItems, renderReadReceipts = renderReadReceipts, newEventState = newItemState.value, @@ -233,6 +237,6 @@ class TimelinePresenter @AssistedInject constructor( } private fun CoroutineScope.paginateBackwards() = launch { - timeline.paginateBackwards(BACK_PAGINATION_EVENT_LIMIT, BACK_PAGINATION_PAGE_SIZE) + timeline.paginateBackwards() } } 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..650b1874b8 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,7 @@ 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 io.element.android.libraries.matrix.api.timeline.Timeline import kotlinx.collections.immutable.ImmutableList @Immutable @@ -29,7 +29,7 @@ data class TimelineState( val timelineRoomInfo: TimelineRoomInfo, val renderReadReceipts: Boolean, val highlightedEventId: EventId?, - val paginationState: MatrixTimeline.PaginationState, + val backPaginationStatus: Timeline.PaginationStatus, val newEventState: NewEventState, val eventSink: (TimelineEvents) -> Unit ) @@ -37,6 +37,7 @@ data class TimelineState( @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 ae7f62ebd7..19513f08fe 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 @@ -34,7 +34,7 @@ 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.Timeline 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 @@ -46,32 +46,31 @@ import kotlin.random.Random fun aTimelineState( timelineItems: ImmutableList = persistentListOf(), - paginationState: MatrixTimeline.PaginationState = aPaginationState(), + paginationState: Timeline.PaginationStatus = aPaginationStatus(), renderReadReceipts: Boolean = false, timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), eventSink: (TimelineEvents) -> Unit = {}, ) = TimelineState( timelineItems = timelineItems, timelineRoomInfo = timelineRoomInfo, - paginationState = paginationState, + backPaginationStatus = paginationState, renderReadReceipts = renderReadReceipts, highlightedEventId = null, newEventState = NewEventState.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, +fun aPaginationStatus( + isPaginating: Boolean = false, + hasMoreToLoad: Boolean = true, +): Timeline.PaginationStatus { + return Timeline.PaginationStatus( + isPaginating = isPaginating, + hasMoreToLoad = hasMoreToLoad, ) } + internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList { return persistentListOf( // 3 items (First Middle Last) with isMine = false @@ -230,10 +229,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..1eab2fd2a3 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,7 +55,6 @@ 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 @@ -79,7 +78,6 @@ import kotlinx.coroutines.launch fun TimelineView( state: TimelineState, typingNotificationState: TypingNotificationState, - roomName: String?, onUserDataClicked: (UserId) -> Unit, onLinkClicked: (String) -> Unit, onMessageClicked: (TimelineItem.Event) -> Unit, @@ -93,9 +91,6 @@ fun TimelineView( modifier: Modifier = Modifier, forceJumpToBottomVisibility: Boolean = false ) { - fun onReachedLoadMore() { - state.eventSink(TimelineEvents.LoadMore) - } fun onScrollFinishedAt(firstVisibleIndex: Int) { state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex)) @@ -152,20 +147,6 @@ 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) - } - } } TimelineScrollHelper( @@ -272,17 +253,16 @@ internal fun TimelineViewPreview( ) { TimelineView( state = aTimelineState(timelineItems), - roomName = null, 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/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 89b223dafe..7c3557b774 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 @@ -51,6 +51,8 @@ internal fun TimelineItemRow( is TimelineItem.Virtual -> { TimelineItemVirtualRow( virtual = timelineItem, + timelineRoomInfo = timelineRoomInfo, + eventSink = eventSink, modifier = modifier, ) } 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..9edc7a9ad4 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 @@ -17,23 +17,39 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect 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.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) + TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name, modifier = modifier) + is TimelineItemLoadingIndicatorModel -> { + TimelineLoadingMoreIndicator() + LaunchedEffect(key1 = virtual.model.timestamp) { + eventSink(TimelineEvents.LoadMore(virtual.model.backwards)) + } + } } } 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..2efe98e5b3 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,9 @@ 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.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 +43,11 @@ 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( + backwards = inner.backwards, + timestamp = inner.timestamp + ) } } } 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..7007c56e0f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingIndicatorModel.kt @@ -0,0 +1,24 @@ +/* + * 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 class TimelineItemLoadingIndicatorModel( + val backwards: Boolean, + 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/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 2485e57172..8d5699d4e0 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 @@ -36,7 +36,6 @@ 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.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.item.event.EventReaction @@ -96,15 +95,15 @@ class TimelinePresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.paginationState.hasMoreToLoadBackwards).isTrue() - assertThat(initialState.paginationState.isBackPaginating).isFalse() + assertThat(initialState.backPaginationStatus.hasMoreToLoadBackwards).isTrue() + assertThat(initialState.backPaginationStatus.isBackPaginating).isFalse() initialState.eventSink.invoke(TimelineEvents.LoadMore) val inPaginationState = awaitItem() - assertThat(inPaginationState.paginationState.isBackPaginating).isTrue() - assertThat(inPaginationState.paginationState.hasMoreToLoadBackwards).isTrue() + assertThat(inPaginationState.backPaginationStatus.isBackPaginating).isTrue() + assertThat(inPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue() val postPaginationState = awaitItem() - assertThat(postPaginationState.paginationState.hasMoreToLoadBackwards).isTrue() - assertThat(postPaginationState.paginationState.isBackPaginating).isFalse() + assertThat(postPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue() + assertThat(postPaginationState.backPaginationStatus.isBackPaginating).isFalse() } } 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..be7df31635 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 @@ -43,7 +43,6 @@ class TimelineViewTest { ) ), typingNotificationState = aTypingNotificationState(), - roomName = null, onUserDataClicked = EnsureNeverCalledWithParam(), onLinkClicked = EnsureNeverCalledWithParam(), onMessageClicked = EnsureNeverCalledWithParam(), @@ -71,7 +70,6 @@ class TimelineViewTest { ) ), typingNotificationState = aTypingNotificationState(), - roomName = null, onUserDataClicked = EnsureNeverCalledWithParam(), onLinkClicked = EnsureNeverCalledWithParam(), onMessageClicked = EnsureNeverCalledWithParam(), 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..952fe97a5a 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 @@ -28,7 +28,7 @@ class PollRepository @Inject constructor( private val room: MatrixRoom, ) { suspend fun getPoll(eventId: EventId): Result = runCatching { - room.timeline + room.liveTimeline .timelineItems .first() .asSequence() 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..339f02af31 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 @@ -33,7 +33,7 @@ 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -49,8 +49,8 @@ class PollHistoryPresenter @Inject constructor( @Composable override fun present(): PollHistoryState { // TODO use room.rememberPollHistory() when working properly? - val timeline = room.timeline - val paginationState by timeline.paginationState.collectAsState() + val timeline = room.liveTimeline + val paginationState by timeline.backPaginationStatus.collectAsState() val pollHistoryItemsFlow = remember { timeline.timelineItems.map { items -> pollHistoryItemFactory.create(items) @@ -61,11 +61,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 +88,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.paginateBackwards() } } 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 761d87445a..77898c7a9a 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 @@ -31,7 +31,7 @@ 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.LiveTimeline import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings @@ -97,7 +97,7 @@ interface MatrixRoom : Closeable { val syncUpdateFlow: StateFlow - val timeline: MatrixTimeline + val liveTimeline: LiveTimeline fun destroy() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/DetachedTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/DetachedTimeline.kt new file mode 100644 index 0000000000..d6abdc515f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/DetachedTimeline.kt @@ -0,0 +1,24 @@ +/* + * 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 + +interface DetachedTimeline : Timeline { + suspend fun paginateForwards(): Result + val forwardPaginationStatus: StateFlow +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/LiveTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/LiveTimeline.kt new file mode 100644 index 0000000000..643b690eee --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/LiveTimeline.kt @@ -0,0 +1,25 @@ +/* + * 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 kotlinx.coroutines.flow.Flow + +interface LiveTimeline: Timeline { + val membershipChangeEventReceived: Flow + suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result +} 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..c0f8110e56 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.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.timeline + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface Timeline : AutoCloseable { + + data class PaginationStatus( + val isPaginating: Boolean, + val hasMoreToLoad: Boolean, + ) { + val canPaginate: Boolean = !isPaginating && hasMoreToLoad + } + + suspend fun paginateBackwards(): Result + val backPaginationStatus: StateFlow + val timelineItems: Flow> +} 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..fbe2a09716 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 @@ -24,4 +24,11 @@ sealed interface VirtualTimelineItem { data object ReadMarker : VirtualTimelineItem data object EncryptedHistoryBanner : VirtualTimelineItem + + data object RoomBeginning: VirtualTimelineItem + + data class LoadingIndicator( + val backwards: Boolean, + val timestamp: Long, + ): VirtualTimelineItem } 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 59e5c99efa..d630a33a4a 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 @@ -42,7 +42,7 @@ 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.LiveTimeline import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings @@ -56,7 +56,7 @@ 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.RustLiveTimeline 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 @@ -159,7 +159,7 @@ class RustMatrixRoom( private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow - override val timeline = createMatrixTimeline(innerTimeline) { + override val liveTimeline = createLiveTimeline(innerTimeline){ _syncUpdateFlow.value = systemClock.epochMillis() } @@ -169,7 +169,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) @@ -184,7 +184,7 @@ class RustMatrixRoom( override fun destroy() { roomCoroutineScope.cancel() - timeline.close() + liveTimeline.close() innerRoom.destroy() roomListItem.destroy() specialModeEventTimelineItem?.destroy() @@ -744,18 +744,24 @@ class RustMatrixRoom( } } - private fun createMatrixTimeline( + private fun createLiveTimeline( timeline: InnerTimeline, onNewSyncedEvent: () -> Unit = {}, - ): MatrixTimeline { - return RustMatrixTimeline( + ): LiveTimeline { + return RustLiveTimeline( isKeyBackupEnabled = isKeyBackupEnabled, matrixRoom = this, + systemClock = systemClock, roomCoroutineScope = roomCoroutineScope, dispatcher = roomDispatcher, lastLoginTimestamp = sessionData.loginTimestamp, onNewSyncedEvent = onNewSyncedEvent, - innerTimeline = timeline, + inner = timeline, + fetchDetailsForEvent = { eventId -> + runCatching { + innerTimeline.getEventTimelineItemByEventId(eventId.value) + } + } ) } 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/RustDetachedTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustDetachedTimeline.kt new file mode 100644 index 0000000000..5d45f5d44f --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustDetachedTimeline.kt @@ -0,0 +1,20 @@ +/* + * 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 + +class RustDetachedTimeline { +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustLiveTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustLiveTimeline.kt new file mode 100644 index 0000000000..5ff770b053 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustLiveTimeline.kt @@ -0,0 +1,198 @@ +/* + * 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.LiveTimeline +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.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.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.coroutineScope +import kotlinx.coroutines.ensureActive +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.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineItem +import timber.log.Timber +import uniffi.matrix_sdk_ui.EventItemOrigin +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 + +class RustLiveTimeline( + private val inner: InnerTimeline, + private val systemClock: SystemClock, + private val roomCoroutineScope: CoroutineScope, + private val isKeyBackupEnabled: Boolean, + private val matrixRoom: MatrixRoom, + private val dispatcher: CoroutineDispatcher, + private val lastLoginTimestamp: Date?, + private val fetchDetailsForEvent: suspend (EventId) -> Result, + private val onNewSyncedEvent: () -> Unit, +) : LiveTimeline { + + 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 timelineItemFactory = MatrixTimelineItemMapper( + fetchDetailsForEvent = fetchDetailsForEvent, + roomCoroutineScope = roomCoroutineScope, + virtualTimelineItemMapper = VirtualTimelineItemMapper(), + eventTimelineItemMapper = EventTimelineItemMapper( + contentMapper = TimelineEventContentMapper( + eventMessageMapper = EventMessageMapper() + ) + ) + ) + + private val timelineDiffProcessor = MatrixTimelineDiffProcessor( + timelineItems = _timelineItems, + timelineItemFactory = timelineItemFactory, + ) + + 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) + } + } + + override suspend fun paginateBackwards(): Result { + initLatch.await() + return runCatching { + if (!canBackPaginate()) throw TimelineException.CannotPaginate + inner.paginateBackwards() + }.onFailure { error -> + if (error is TimelineException.CannotPaginate) { + Timber.d("Can't paginate backwards on room ${matrixRoom.roomId} with backPaginationStatus: ${backPaginationStatus.value}") + } else { + Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}") + } + }.onSuccess { + Timber.v("Success back paginating for room ${matrixRoom.roomId}") + } + } + + private fun canBackPaginate(): Boolean { + return isInit.get() && backPaginationStatus.value.canPaginate + } + + override val backPaginationStatus: StateFlow = inner + .backPaginationStatusFlow() + .map() + .stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)) + + override val timelineItems: Flow> = combine( + _timelineItems, + backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged() + ) { timelineItems, hasMoreToLoadBackward -> + timelineItems + .let { items -> encryptedHistoryPostProcessor.process(items) } + .let { items -> + roomBeginningPostProcessor.process( + items = items, + isDm = matrixRoom.isDm, + hasMoreToLoadBackwards = hasMoreToLoadBackward + ) + }.let {items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward)} + + } + + override fun close() { + inner.close() + } + + 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) + } +} 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 index b507690a4a..b6f4284bc8 100644 --- 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 @@ -18,16 +18,14 @@ 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.RoomBeginningPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher @@ -39,7 +37,6 @@ 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 @@ -50,224 +47,9 @@ 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.BackPaginationStatus import uniffi.matrix_sdk_ui.EventItemOrigin 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) { - BackPaginationStatus.IDLE -> { - MatrixTimeline.PaginationState( - isBackPaginating = false, - hasMoreToLoadBackwards = true, - beginningOfRoomReached = false, - ) - } - BackPaginationStatus.PAGINATING -> { - MatrixTimeline.PaginationState( - isBackPaginating = true, - hasMoreToLoadBackwards = true, - beginningOfRoomReached = false, - ) - } - BackPaginationStatus.TIMELINE_START_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(paginationOptions) - }.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}") - } - } - - 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/TimelineFlows.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt new file mode 100644 index 0000000000..482489a517 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.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.libraries.matrix.impl.timeline + +import io.element.android.libraries.matrix.api.timeline.Timeline +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import uniffi.matrix_sdk_ui.PaginationStatus + +fun Flow.map(): Flow = map { paginationStatus -> + when (paginationStatus) { + PaginationStatus.IDLE -> Timeline.PaginationStatus( + isPaginating = false, + hasMoreToLoad = true + ) + PaginationStatus.PAGINATING -> Timeline.PaginationStatus( + isPaginating = true, + hasMoreToLoad = true + ) + PaginationStatus.TIMELINE_END_REACHED -> Timeline.PaginationStatus( + isPaginating = false, + hasMoreToLoad = false + ) + } +} + 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..882d89bbee --- /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 + +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/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..2d08c8e5b8 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt @@ -0,0 +1,44 @@ +/* + * 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 +import io.element.android.services.toolbox.api.systemclock.SystemClock + +class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) { + + fun process( + items: List, + hasMoreToLoadBackwards: Boolean, + ): List { + return if (hasMoreToLoadBackwards && !items.hasEncryptionHistoryBanner()){ + listOf( + MatrixTimelineItem.Virtual( + uniqueId = "BackwardLoadingIndicator", + virtual = VirtualTimelineItem.LoadingIndicator( + backwards = true, + timestamp = systemClock.epochMillis() + ) + ) + ) + items + }else { + items + } + } + +} 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 73% 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..fed2266659 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 @@ -21,17 +21,36 @@ 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 /** - * 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 = MatrixTimelineItem.Virtual( + uniqueId = VirtualTimelineItem.RoomBeginning.toString(), + virtual = VirtualTimelineItem.RoomBeginning + ) + return listOf(roomBeginningItem) + items + } + + private fun processForDM(items: List): List { // Find room creation event. This is usually index 0 val roomCreationEventIndex = items.indexOfFirst { 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/DmBeginningTimelineProcessorTest.kt index fc24f54c14..40a15f7621 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/DmBeginningTimelineProcessorTest.kt @@ -35,8 +35,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 = true) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEmpty() } @@ -52,8 +52,8 @@ 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 = true) assertThat(processedItems).isEqualTo(expected) } @@ -63,8 +63,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 = false, isAtStartOfTimeline = true) + val processor = RoomBeginningPostProcessor() + val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -74,8 +74,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 = false) assertThat(processedItems).isEqualTo(timelineItems) } @@ -84,8 +84,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 = false) assertThat(processedItems).isEqualTo(timelineItems) } @@ -95,8 +95,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 = false) 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 5938cd300b..c8dad4ad8a 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 @@ -42,7 +42,6 @@ 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.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver 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 index f9ab698b8e..f98e8d0fcb 100644 --- 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 @@ -17,7 +17,6 @@ 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 From f56ac869ffe3b9adbee0b396204ac3b6e03e68b1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Apr 2024 13:30:55 +0200 Subject: [PATCH 03/53] Timeline permalink : continue to iterate (try a strategy to avoid forward insertion to "auto-scroll") --- .../features/messages/impl/MessagesNode.kt | 14 +- .../impl/timeline/TimelineController.kt | 135 ++++++++++++++++++ .../messages/impl/timeline/TimelineEvents.kt | 6 +- .../impl/timeline/TimelineItemIndexer.kt | 65 +++++++++ .../impl/timeline/TimelinePresenter.kt | 99 +++++++++---- .../messages/impl/timeline/TimelineState.kt | 14 +- .../impl/timeline/TimelineStateProvider.kt | 5 +- .../messages/impl/timeline/TimelineView.kt | 98 +++++++++++-- .../components/TimelineItemVirtualRow.kt | 7 +- .../virtual/TimelineLoadingMoreIndicator.kt | 13 +- .../factories/TimelineItemsFactory.kt | 17 ++- .../virtual/TimelineItemVirtualFactory.kt | 4 +- .../TimelineItemInvisibleIndicatorModel.kt | 5 +- .../TimelineItemLoadingIndicatorModel.kt | 4 +- .../poll/impl/history/PollHistoryPresenter.kt | 4 +- .../libraries/matrix/api/room/MatrixRoom.kt | 6 +- .../matrix/api/timeline/DetachedTimeline.kt | 24 ---- .../matrix/api/timeline/LiveTimeline.kt | 25 ---- .../libraries/matrix/api/timeline/Timeline.kt | 12 +- .../item/virtual/VirtualTimelineItem.kt | 6 +- .../matrix/impl/room/RustMatrixRoom.kt | 20 ++- .../impl/timeline/RustMatrixTimeline.kt | 55 ------- .../{RustLiveTimeline.kt => RustTimeline.kt} | 58 ++++++-- .../InvisibleIndicatorPostProcessor.kt | 79 ++++++++++ .../LoadingIndicatorsPostProcessor.kt | 42 ++++-- 25 files changed, 599 insertions(+), 218 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt rename libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustDetachedTimeline.kt => features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemInvisibleIndicatorModel.kt (73%) delete mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/DetachedTimeline.kt delete mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/LiveTimeline.kt delete mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt rename libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/{RustLiveTimeline.kt => RustTimeline.kt} (73%) create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/InvisibleIndicatorPostProcessor.kt 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..70e077eb04 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 @@ -30,6 +30,7 @@ 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.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 @@ -116,6 +117,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 +126,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 +135,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,7 +191,7 @@ 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, 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..e4ae628263 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -0,0 +1,135 @@ +/* + * 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 androidx.compose.runtime.MutableState +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 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.ReceiptType +import io.element.android.libraries.matrix.api.timeline.Timeline +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import java.util.Optional +import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException + +@SingleIn(RoomScope::class) +class TimelineController @Inject constructor( + private val room: MatrixRoom, +) { + + private val liveTimeline = MutableStateFlow(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 focusOnEvent(eventId: EventId): Result { + return try { + val newDetachedTimeline = room.timelineFocusedOnEvent(eventId) + detachedTimeline.getAndUpdate { current -> + if (current.isPresent) { + current.get().close() + } + Optional.of(newDetachedTimeline) + } + Result.success(Unit) + } catch (cancellation: CancellationException) { + throw cancellation + } catch (exception: Exception) { + Result.failure(exception) + } + } + + /** + * Makes sure the controller is focused on the live timeline. + * This does close the detached timeline if any. + */ + fun focusOnLive() { + detachedTimeline.getAndUpdate { + when { + it.isPresent -> { + it.get().close() + Optional.empty() + } + else -> Optional.empty() + } + } + } + + suspend fun paginate(direction: Timeline.PaginationDirection): Result { + return currentTimelineFlow().first().paginate(direction) + } + + private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached -> + when { + detached.isPresent -> detached.get() + else -> live + } + } + + suspend fun sendReadReceiptIfNeeded( + firstVisibleIndex: Int, + timelineItems: ImmutableList, + lastReadReceiptId: MutableState, + readReceiptType: ReceiptType, + ) { + // If we are at the bottom of timeline, we mark the room as read. + if (firstVisibleIndex == 0) { + room.markAsRead(receiptType = readReceiptType) + } else { + // Get last valid EventId seen by the user, as the first index might refer to a Virtual item + val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) + if (eventId != null && eventId != lastReadReceiptId.value) { + lastReadReceiptId.value = eventId + currentTimelineFlow() + .filterIsInstance(Timeline::class) + .first() + .sendReadReceipt(eventId = eventId, receiptType = readReceiptType) + } + } + } + + private fun getLastEventIdBeforeOrAt(index: Int, items: ImmutableList): EventId? { + for (i in index until items.count()) { + val item = items[i] + if (item is TimelineItem.Event) { + return item.eventId + } + } + return null + } +} 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 62ec074bea..834b80489b 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 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 backwards: Boolean) : EventFromTimelineItem + 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..4507dea99d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt @@ -0,0 +1,65 @@ +/* + * 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 eede76dae9..8b8f73eb1a 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 @@ -56,6 +56,7 @@ import kotlinx.coroutines.withContext class TimelinePresenter @AssistedInject constructor( private val timelineItemsFactory: TimelineItemsFactory, + private val timelineItemIndexer: TimelineItemIndexer, private val room: MatrixRoom, private val dispatchers: CoroutineDispatchers, private val appScope: CoroutineScope, @@ -64,14 +65,13 @@ 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.liveTimeline - @Composable override fun present(): TimelineState { val localScope = rememberCoroutineScope() @@ -79,41 +79,52 @@ class TimelinePresenter @AssistedInject constructor( mutableStateOf(null) } + 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.backPaginationStatus.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) { is TimelineEvents.LoadMore -> { - if(event.backwards) { - localScope.paginateBackwards() - }else{ - //TODO implement pagination forward + localScope.launch { + timelineController.paginate(direction = event.direction) } } is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId is TimelineEvents.OnScrollFinished -> { - if (event.firstIndex == 0) { - newItemState.value = NewEventState.None + if (isLive) { + if (event.firstIndex == 0) { + newEventState.value = NewEventState.None + } + 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( @@ -126,28 +137,55 @@ 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.None + }, + onFailure = { + focusRequestState.value = FocusRequestState.Failure(it) + } + ) + } + } + is TimelineEvents.ClearFocusRequestState -> { + focusRequestState.value = FocusRequestState.None + } + is TimelineEvents.JumpToLive -> { + localScope.launch { + timelineController.focusOnLive() + } + } } } + // Makes sure to get back to live when there is nothing more to load forwards + LaunchedEffect(isLive) { + + } + LaunchedEffect(timelineItems.size) { - computeNewItemState(timelineItems, prevMostRecentItemId, newItemState) + computeNewItemState(timelineItems, prevMostRecentItemId, newEventState) } 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) } @@ -165,10 +203,12 @@ class TimelinePresenter @AssistedInject constructor( return TimelineState( timelineRoomInfo = timelineRoomInfo, highlightedEventId = highlightedEventId.value, - backPaginationStatus = paginationState, timelineItems = timelineItems, renderReadReceipts = renderReadReceipts, - newEventState = newItemState.value, + newEventState = newEventState.value, + isLive = isLive, + focusedEventId = focusedEventId.value, + focusRequestState = focusRequestState.value, eventSink = { handleEvents(it) } ) } @@ -194,6 +234,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 @@ -221,7 +262,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) + //timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) } } } @@ -235,8 +276,4 @@ class TimelinePresenter @AssistedInject constructor( } return null } - - private fun CoroutineScope.paginateBackwards() = launch { - timeline.paginateBackwards() - } } 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 650b1874b8..c7a077a721 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.Timeline import kotlinx.collections.immutable.ImmutableList @Immutable @@ -29,11 +28,20 @@ data class TimelineState( val timelineRoomInfo: TimelineRoomInfo, val renderReadReceipts: Boolean, val highlightedEventId: EventId?, - val backPaginationStatus: Timeline.PaginationStatus, val newEventState: NewEventState, - val eventSink: (TimelineEvents) -> Unit + val isLive: Boolean, + val focusedEventId : EventId?, + val focusRequestState: FocusRequestState, + val eventSink: (TimelineEvents) -> Unit, ) +sealed interface FocusRequestState { + data object None : FocusRequestState + data class Cached(val index: Int): FocusRequestState + data object Fetching : FocusRequestState + data class Failure(val throwable: Throwable) : FocusRequestState +} + @Immutable data class TimelineRoomInfo( val isDm: 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 19513f08fe..201c12837a 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 @@ -46,17 +46,18 @@ import kotlin.random.Random fun aTimelineState( timelineItems: ImmutableList = persistentListOf(), - paginationState: Timeline.PaginationStatus = aPaginationStatus(), renderReadReceipts: Boolean = false, timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), eventSink: (TimelineEvents) -> Unit = {}, ) = TimelineState( timelineItems = timelineItems, timelineRoomInfo = timelineRoomInfo, - backPaginationStatus = paginationState, renderReadReceipts = renderReadReceipts, highlightedEventId = null, newEventState = NewEventState.None, + isLive = true, + focusedEventId = null, + focusRequestState = FocusRequestState.None, eventSink = eventSink, ) 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 1eab2fd2a3..e3dfa248ad 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 @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline import android.view.accessibility.AccessibilityManager +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween @@ -55,7 +56,6 @@ 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.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.model.NewEventState @@ -63,8 +63,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.typing.TypingNotificationState -import io.element.android.features.messages.impl.typing.TypingNotificationView import io.element.android.features.messages.impl.typing.aTypingNotificationState +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.designsystem.theme.components.FloatingActionButton @@ -73,6 +74,8 @@ 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 timber.log.Timber +import kotlin.math.abs @Composable fun TimelineView( @@ -92,6 +95,10 @@ fun TimelineView( forceJumpToBottomVisibility: Boolean = false ) { + fun clearFocusRequestState() { + state.eventSink(TimelineEvents.ClearFocusRequestState) + } + fun onScrollFinishedAt(firstVisibleIndex: Int) { state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex)) } @@ -109,6 +116,10 @@ fun TimelineView( // TODO implement this logic once we have support to 'jump to event X' in sliding sync } + LaunchedEffect(key1 = state.timelineItems) { + Timber.d("TimelineView - timelineItem identifiers: ${state.timelineItems.joinToString(", ") { it.identifier() }}") + } + // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms AnimatedVisibility(visible = true, enter = fadeIn()) { Box(modifier) { @@ -118,9 +129,12 @@ fun TimelineView( reverseLayout = useReverseLayout, contentPadding = PaddingValues(vertical = 8.dp), ) { - item { + /* + item(key = UUID.randomUUID()) { TypingNotificationView(state = typingNotificationState) } + + */ items( items = state.timelineItems, contentType = { timelineItem -> timelineItem.contentType() }, @@ -149,30 +163,68 @@ fun TimelineView( } } + FocusRequestStateView( + focusRequestState = state.focusRequestState, + onClearFocusRequestState = ::clearFocusRequestState + ) + TimelineScrollHelper( isTimelineEmpty = state.timelineItems.isEmpty(), lazyListState = lazyListState, forceJumpToBottomVisibility = forceJumpToBottomVisibility, newEventState = state.newEventState, - onScrollFinishedAt = ::onScrollFinishedAt + isLive = state.isLive, + focusRequestState = state.focusRequestState, + onScrollFinishedAt = ::onScrollFinishedAt, + onClearFocusRequestState = ::clearFocusRequestState, + onJumpToLive = { state.eventSink(TimelineEvents.JumpToLive) }, ) } } } +@Composable +private fun FocusRequestStateView( + focusRequestState: FocusRequestState, + onClearFocusRequestState: () -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler(enabled = focusRequestState is FocusRequestState.Fetching) { + onClearFocusRequestState() + } + + when (focusRequestState) { + is FocusRequestState.Failure -> { + ErrorDialog( + content = stringResource(id = CommonStrings.common_failed), + onDismiss = onClearFocusRequestState, + modifier = modifier, + ) + } + FocusRequestState.Fetching -> { + ProgressDialog(modifier = modifier) + } + is FocusRequestState.Cached, FocusRequestState.None -> Unit + } +} + @Composable private fun BoxScope.TimelineScrollHelper( isTimelineEmpty: 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 } } @@ -186,9 +238,29 @@ private fun BoxScope.TimelineScrollHelper( } } + fun jumpToBottom() { + if (isLive) { + scrollToBottom() + } else { + onJumpToLive() + } + } + + LaunchedEffect(key1 = focusRequestState) { + if (focusRequestState is FocusRequestState.Cached) { + if (abs(lazyListState.firstVisibleItemIndex - focusRequestState.index) < 10) { + lazyListState.animateScrollToItem(focusRequestState.index) + } else { + lazyListState.scrollToItem(focusRequestState.index) + } + onClearFocusRequestState() + } + } + LaunchedEffect(canAutoScroll, newEventState) { - val shouldAutoScroll = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe) - if (shouldAutoScroll) { + Timber.d("TimelineScrollHelper - canAutoScroll: $canAutoScroll, newEventState: $newEventState") + val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe) + if (shouldScrollToBottom) { scrollToBottom() } } @@ -203,11 +275,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, + .align(Alignment.BottomEnd) + .padding(end = 24.dp, bottom = 12.dp), + onClick = { jumpToBottom() }, ) } @@ -233,8 +305,8 @@ private fun JumpToBottomButton( ) { Icon( modifier = Modifier - .size(24.dp) - .rotate(90f), + .size(24.dp) + .rotate(90f), imageVector = CompoundIcons.ArrowRight(), contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom) ) 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 9edc7a9ad4..4235c7e065 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,6 +16,7 @@ package io.element.android.features.messages.impl.timeline.components +import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier @@ -29,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.components.virtual.Tim 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.TimelineItemInvisibleIndicatorModel 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 @@ -46,10 +48,11 @@ fun TimelineItemVirtualRow( is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier) TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name, modifier = modifier) is TimelineItemLoadingIndicatorModel -> { - TimelineLoadingMoreIndicator() + TimelineLoadingMoreIndicator(modifier) LaunchedEffect(key1 = virtual.model.timestamp) { - eventSink(TimelineEvents.LoadMore(virtual.model.backwards)) + eventSink(TimelineEvents.LoadMore(virtual.model.direction)) } } + TimelineItemInvisibleIndicatorModel -> 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..cf5c54f3d4 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 @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.components.virtual import androidx.compose.foundation.layout.Box 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 @@ -26,19 +27,19 @@ import androidx.compose.ui.Modifier 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 @Composable internal fun TimelineLoadingMoreIndicator(modifier: Modifier = Modifier) { Box( - modifier + modifier = modifier .fillMaxWidth() - .wrapContentHeight() - .padding(8.dp), + .padding(2.dp), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator( - strokeWidth = 2.dp, + LinearProgressIndicator(modifier = Modifier + .height(1.dp) + .fillMaxWidth() ) } } 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..34049fbaf7 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 @@ -33,6 +34,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -43,9 +45,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( @@ -60,6 +62,10 @@ class TimelineItemsFactory @Inject constructor( } } + fun items(): StateFlow> { + return timelineItems + } + @Composable fun collectItemsAsState(): State> { return timelineItems.collectAsState() @@ -80,6 +86,7 @@ class TimelineItemsFactory @Inject constructor( roomMembers: List, ) { val newTimelineItemStates = ArrayList() + val newTimelineById = mutableMapOf() for (index in diffCache.indices().reversed()) { val cacheItem = diffCache.get(index) if (cacheItem == null) { @@ -96,10 +103,12 @@ class TimelineItemsFactory @Inject constructor( } else { cacheItem } + newTimelineById[updatedItem.identifier()] = updatedItem newTimelineItemStates.add(updatedItem) } } val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList() + timelineItemIndexer.process(result) this.timelineItems.emit(result) } @@ -108,13 +117,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 2efe98e5b3..e6df9e889d 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,6 +18,7 @@ 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.TimelineItemInvisibleIndicatorModel 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 @@ -45,9 +46,10 @@ class TimelineItemVirtualFactory @Inject constructor( is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel( - backwards = inner.backwards, + direction = inner.direction, timestamp = inner.timestamp ) + VirtualTimelineItem.LatestKnownEventIndicator -> TimelineItemInvisibleIndicatorModel } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustDetachedTimeline.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemInvisibleIndicatorModel.kt similarity index 73% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustDetachedTimeline.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemInvisibleIndicatorModel.kt index 5d45f5d44f..2f4a118746 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustDetachedTimeline.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemInvisibleIndicatorModel.kt @@ -14,7 +14,8 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.impl.timeline +package io.element.android.features.messages.impl.timeline.model.virtual -class RustDetachedTimeline { +data object TimelineItemInvisibleIndicatorModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemInvisibleIndicatorModel" } 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 index 7007c56e0f..da022d6d59 100644 --- 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 @@ -16,8 +16,10 @@ package io.element.android.features.messages.impl.timeline.model.virtual +import io.element.android.libraries.matrix.api.timeline.Timeline + data class TimelineItemLoadingIndicatorModel( - val backwards: Boolean, + val direction: Timeline.PaginationDirection, val timestamp: Long, ) : TimelineItemVirtualModel { override val type: String = "TimelineItemLoadingIndicatorModel" 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 339f02af31..b36359aa25 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 @@ -50,7 +50,7 @@ class PollHistoryPresenter @Inject constructor( override fun present(): PollHistoryState { // TODO use room.rememberPollHistory() when working properly? val timeline = room.liveTimeline - val paginationState by timeline.backPaginationStatus.collectAsState() + val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState() val pollHistoryItemsFlow = remember { timeline.timelineItems.map { items -> pollHistoryItemFactory.create(items) @@ -96,6 +96,6 @@ class PollHistoryPresenter @Inject constructor( } private fun CoroutineScope.loadMore(pollHistory: Timeline) = launch { - pollHistory.paginateBackwards() + pollHistory.paginate(Timeline.PaginationDirection.BACKWARDS) } } 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 8e32a51a5c..a2ca077bb8 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,7 +32,7 @@ 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.LiveTimeline +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings @@ -98,7 +98,9 @@ interface MatrixRoom : Closeable { val syncUpdateFlow: StateFlow - val liveTimeline: LiveTimeline + val liveTimeline: Timeline + + suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline fun destroy() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/DetachedTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/DetachedTimeline.kt deleted file mode 100644 index d6abdc515f..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/DetachedTimeline.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 - -interface DetachedTimeline : Timeline { - suspend fun paginateForwards(): Result - val forwardPaginationStatus: StateFlow -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/LiveTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/LiveTimeline.kt deleted file mode 100644 index 643b690eee..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/LiveTimeline.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 kotlinx.coroutines.flow.Flow - -interface LiveTimeline: Timeline { - val membershipChangeEventReceived: Flow - 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 index c0f8110e56..05b1549ae1 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -16,6 +16,7 @@ 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 @@ -28,7 +29,14 @@ interface Timeline : AutoCloseable { val canPaginate: Boolean = !isPaginating && hasMoreToLoad } - suspend fun paginateBackwards(): Result - val backPaginationStatus: StateFlow + 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> } 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 fbe2a09716..2879225553 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 @@ -27,8 +29,10 @@ sealed interface VirtualTimelineItem { data object RoomBeginning: VirtualTimelineItem + data object LatestKnownEventIndicator: VirtualTimelineItem + data class LoadingIndicator( - val backwards: Boolean, + val direction: Timeline.PaginationDirection, val timestamp: Long, ): VirtualTimelineItem } 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 03f14eef7a..147ca3f2da 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 @@ -43,7 +43,7 @@ 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.LiveTimeline +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings @@ -57,7 +57,7 @@ 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.RustLiveTimeline +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 @@ -160,7 +160,7 @@ class RustMatrixRoom( private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow - override val liveTimeline = createLiveTimeline(innerTimeline){ + override val liveTimeline = createTimeline(innerTimeline, isLive = true){ _syncUpdateFlow.value = systemClock.epochMillis() } @@ -183,6 +183,12 @@ class RustMatrixRoom( override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId) + override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline { + return innerRoom.timelineFocusedOnEvent(eventId.value, numContextEvents = 50u).let {inner -> + createTimeline(inner, isLive = false){} + } + } + override fun destroy() { roomCoroutineScope.cancel() liveTimeline.close() @@ -745,12 +751,14 @@ class RustMatrixRoom( } } - private fun createLiveTimeline( + private fun createTimeline( timeline: InnerTimeline, + isLive: Boolean = true, onNewSyncedEvent: () -> Unit = {}, - ): LiveTimeline { - return RustLiveTimeline( + ): Timeline { + return RustTimeline( isKeyBackupEnabled = isKeyBackupEnabled, + isLive = isLive, matrixRoom = this, systemClock = systemClock, roomCoroutineScope = roomCoroutineScope, 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 b6f4284bc8..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ /dev/null @@ -1,55 +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.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.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.RoomBeginningPostProcessor -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.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 java.util.Date -import java.util.concurrent.atomic.AtomicBoolean - -private const val INITIAL_MAX_SIZE = 50 - diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustLiveTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt similarity index 73% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustLiveTimeline.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 5ff770b053..13a8e40161 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustLiveTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -18,7 +18,6 @@ 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.LiveTimeline 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 @@ -27,6 +26,7 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage 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.InvisibleIndicatorPostProcessor 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 @@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -58,8 +59,9 @@ import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline private const val INITIAL_MAX_SIZE = 50 -class RustLiveTimeline( +class RustTimeline( private val inner: InnerTimeline, + private val isLive: Boolean, private val systemClock: SystemClock, private val roomCoroutineScope: CoroutineScope, private val isKeyBackupEnabled: Boolean, @@ -68,7 +70,7 @@ class RustLiveTimeline( private val lastLoginTimestamp: Date?, private val fetchDetailsForEvent: suspend (EventId) -> Result, private val onNewSyncedEvent: () -> Unit, -) : LiveTimeline { +) : Timeline { private val initLatch = CompletableDeferred() private val isInit = AtomicBoolean(false) @@ -85,6 +87,7 @@ class RustLiveTimeline( private val roomBeginningPostProcessor = RoomBeginningPostProcessor() private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock) + private val invisibleIndicatorPostProcessor = InvisibleIndicatorPostProcessor(isLive) private val timelineItemFactory = MatrixTimelineItemMapper( fetchDetailsForEvent = fetchDetailsForEvent, @@ -127,35 +130,59 @@ class RustLiveTimeline( } } - override suspend fun paginateBackwards(): Result { + override suspend fun paginate(direction: Timeline.PaginationDirection): Result { initLatch.await() return runCatching { - if (!canBackPaginate()) throw TimelineException.CannotPaginate - inner.paginateBackwards() + if (!canPaginate(direction)) throw TimelineException.CannotPaginate + when (direction) { + Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(50u) + Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(50u) + } }.onFailure { error -> if (error is TimelineException.CannotPaginate) { - Timber.d("Can't paginate backwards on room ${matrixRoom.roomId} with backPaginationStatus: ${backPaginationStatus.value}") + Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}") } else { - Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}") + Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}") } }.onSuccess { - Timber.v("Success back paginating for room ${matrixRoom.roomId}") + Timber.v("Success paginating $direction for room ${matrixRoom.roomId}") } } - private fun canBackPaginate(): Boolean { - return isInit.get() && backPaginationStatus.value.canPaginate + 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 val backPaginationStatus: StateFlow = inner + override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow { + return when (direction) { + Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus + Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus + } + } + + private val backPaginationStatus: StateFlow = inner .backPaginationStatusFlow() .map() .stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)) + private val forwardPaginationStatus: StateFlow = + when (isLive) { + true -> MutableStateFlow(Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = false)) + false -> inner + .forwardPaginationStatusFlow() + .map() + .stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)) + } + override val timelineItems: Flow> = combine( _timelineItems, - backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged() - ) { timelineItems, hasMoreToLoadBackward -> + backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(), + forwardPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(), + ) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward -> timelineItems .let { items -> encryptedHistoryPostProcessor.process(items) } .let { items -> @@ -164,7 +191,8 @@ class RustLiveTimeline( isDm = matrixRoom.isDm, hasMoreToLoadBackwards = hasMoreToLoadBackward ) - }.let {items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward)} + }.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) } + .let { items -> invisibleIndicatorPostProcessor.process(items) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/InvisibleIndicatorPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/InvisibleIndicatorPostProcessor.kt new file mode 100644 index 0000000000..7c7f819ed7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/InvisibleIndicatorPostProcessor.kt @@ -0,0 +1,79 @@ +/* + * 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 + +class InvisibleIndicatorPostProcessor( + private val isLive: Boolean, +) { + private val latestEventIdentifiers: MutableSet = HashSet() + + fun process( + items: List, + ): List { + if (isLive) { + return items + } else { + return buildList { + items.forEach { item -> + add(item) + if (item is MatrixTimelineItem.Event) { + if (latestEventIdentifiers.contains(item.uniqueId)) { + add(createLatestKnownEventIndicator(item.uniqueId)) + } + } + } + items.latestEventIdentifier()?.let { latestEventIdentifier -> + if (latestEventIdentifiers.add(latestEventIdentifier)) { + add(createLatestKnownEventIndicator(latestEventIdentifier)) + } + } + } + } + } + + private fun createLatestKnownEventIndicator(identifier: String): MatrixTimelineItem { + return MatrixTimelineItem.Virtual( + uniqueId = "latest_known_event_$identifier", + virtual = VirtualTimelineItem.LatestKnownEventIndicator + ) + } + + private fun List.latestEventIdentifier(): String? { + return findLast { + when (it) { + is MatrixTimelineItem.Event -> true + else -> false + } + }?.let { + (it as MatrixTimelineItem.Event).uniqueId + } + } + + private fun List.indexOf(identifier: String): Int { + return indexOfLast { + when (it) { + is MatrixTimelineItem.Event -> { + it.uniqueId == identifier + } + else -> false + } + } + } +} 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 index 2d08c8e5b8..c2db96730b 100644 --- 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 @@ -17,6 +17,7 @@ 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 @@ -24,21 +25,34 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) { fun process( items: List, - hasMoreToLoadBackwards: Boolean, + hasMoreToLoadBackward: Boolean, + hasMoreToLoadForward: Boolean, ): List { - return if (hasMoreToLoadBackwards && !items.hasEncryptionHistoryBanner()){ - listOf( - MatrixTimelineItem.Virtual( - uniqueId = "BackwardLoadingIndicator", - virtual = VirtualTimelineItem.LoadingIndicator( - backwards = true, - timestamp = systemClock.epochMillis() - ) + 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 + ) ) - ) + items - }else { - items - } + add(backwardLoadingIndicator) + } + addAll(items) + if (shouldAddForwardLoadingIndicator) { + val forwardLoadingIndicator = MatrixTimelineItem.Virtual( + uniqueId = "ForwardLoadingIndicator", + virtual = VirtualTimelineItem.LoadingIndicator( + direction = Timeline.PaginationDirection.FORWARDS, + timestamp = currentTimestamp + ) + ) + add(forwardLoadingIndicator) + } + } } - } From e09e23abb04f2b949571945d0ddefac6f0791b10 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Apr 2024 13:45:41 +0200 Subject: [PATCH 04/53] Timeline permalink : automatic focus on live when reaching end of forward pagination (and remove usage of PaginationStatus) --- .../impl/timeline/TimelineController.kt | 10 +++- .../impl/timeline/RoomTimelineExtensions.kt | 28 ----------- .../matrix/impl/timeline/RustTimeline.kt | 48 +++++++++++-------- .../matrix/impl/timeline/TimelineFlows.kt | 40 ---------------- 4 files changed, 36 insertions(+), 90 deletions(-) delete mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index e4ae628263..9f98614fac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -35,11 +35,14 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import java.util.Optional import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException +/** + * This controller is responsible of using the right timeline to display messages. + * It can be focused on the live timeline or on a detached timeline (focusing an unknown event). + */ @SingleIn(RoomScope::class) class TimelineController @Inject constructor( private val room: MatrixRoom, @@ -92,6 +95,11 @@ class TimelineController @Inject constructor( suspend fun paginate(direction: Timeline.PaginationDirection): Result { return currentTimelineFlow().first().paginate(direction) + .onSuccess { hasReachedEnd -> + if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) { + focusOnLive() + } + } } private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached -> 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 400ff8fffd..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,14 +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.TaskHandle 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 { @@ -59,29 +54,6 @@ internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List = - paginationStatusFlow { listener -> - subscribeToBackPaginationStatus(listener) - } - -internal fun Timeline.forwardPaginationStatusFlow(): Flow = - paginationStatusFlow { listener -> - subscribeToForwardPaginationStatus(listener) - } - -private fun paginationStatusFlow(subscriber: suspend (PaginationStatusListener)->TaskHandle): Flow{ - return mxCallbackFlow { - val listener = object : PaginationStatusListener { - override fun onUpdate(status: PaginationStatus) { - trySendBlocking(status) - } - } - tryOrNull { - subscriber(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/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 13a8e40161..a3c121fd97 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIn 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.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -38,15 +39,13 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.ensureActive 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.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.TimelineDiff @@ -58,6 +57,7 @@ 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, @@ -105,6 +105,14 @@ class RustTimeline( 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 -> @@ -130,22 +138,34 @@ class RustTimeline( } } + 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) + } + } + override suspend fun paginate(direction: Timeline.PaginationDirection): Result { initLatch.await() return runCatching { if (!canPaginate(direction)) throw TimelineException.CannotPaginate + updatePaginationStatus(direction) { it.copy(isPaginating = true) } when (direction) { - Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(50u) - Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(50u) + Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(PAGINATION_SIZE.toUShort()) + Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(PAGINATION_SIZE.toUShort()) } }.onFailure { error -> + updatePaginationStatus(direction) { it.copy(isPaginating = false) } + if (error is CancellationException) { + throw error + } 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 { - Timber.v("Success paginating $direction for room ${matrixRoom.roomId}") + }.onSuccess { hasReachedEnd -> + updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) } } } @@ -164,20 +184,6 @@ class RustTimeline( } } - private val backPaginationStatus: StateFlow = inner - .backPaginationStatusFlow() - .map() - .stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)) - - private val forwardPaginationStatus: StateFlow = - when (isLive) { - true -> MutableStateFlow(Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = false)) - false -> inner - .forwardPaginationStatusFlow() - .map() - .stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)) - } - override val timelineItems: Flow> = combine( _timelineItems, backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(), diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt deleted file mode 100644 index 482489a517..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.timeline.Timeline -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import uniffi.matrix_sdk_ui.PaginationStatus - -fun Flow.map(): Flow = map { paginationStatus -> - when (paginationStatus) { - PaginationStatus.IDLE -> Timeline.PaginationStatus( - isPaginating = false, - hasMoreToLoad = true - ) - PaginationStatus.PAGINATING -> Timeline.PaginationStatus( - isPaginating = true, - hasMoreToLoad = true - ) - PaginationStatus.TIMELINE_END_REACHED -> Timeline.PaginationStatus( - isPaginating = false, - hasMoreToLoad = false - ) - } -} - From 0480f8d1bcb1e85105bb0a6b8f55e4ec494725ab Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Apr 2024 15:04:49 +0200 Subject: [PATCH 05/53] Timeline : add box around TimelineItemVirtualRow --- .../components/TimelineItemVirtualRow.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) 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 4235c7e065..9dd1210af0 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,6 +16,7 @@ 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 @@ -42,17 +43,19 @@ fun TimelineItemVirtualRow( eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier ) { - when (virtual.model) { - is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier) - TimelineItemReadMarkerModel -> TimelineItemReadMarkerView() - is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier) - TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name, modifier = modifier) - is TimelineItemLoadingIndicatorModel -> { - TimelineLoadingMoreIndicator(modifier) - LaunchedEffect(key1 = virtual.model.timestamp) { - eventSink(TimelineEvents.LoadMore(virtual.model.direction)) + Box(modifier = modifier) { + when (virtual.model) { + is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model) + TimelineItemReadMarkerModel -> TimelineItemReadMarkerView() + is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView() + TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name) + is TimelineItemLoadingIndicatorModel -> { + TimelineLoadingMoreIndicator() + LaunchedEffect(key1 = virtual.model.timestamp) { + eventSink(TimelineEvents.LoadMore(virtual.model.direction)) + } } + TimelineItemInvisibleIndicatorModel -> Spacer(Modifier) } - TimelineItemInvisibleIndicatorModel -> Spacer(modifier = modifier) } } From 64d02d17bb95e377dcca726bd88a7d681dd2356f Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Apr 2024 15:09:01 +0200 Subject: [PATCH 06/53] Timeline : remove highlightedEventId to only use focusedEventId --- .../android/features/messages/impl/MessagesPresenter.kt | 4 ---- .../features/messages/impl/timeline/TimelineEvents.kt | 1 - .../features/messages/impl/timeline/TimelinePresenter.kt | 6 ------ .../features/messages/impl/timeline/TimelineState.kt | 1 - .../messages/impl/timeline/TimelineStateProvider.kt | 1 - .../android/features/messages/impl/timeline/TimelineView.kt | 2 +- 6 files changed, 1 insertion(+), 14 deletions(-) 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..6531c0f8d5 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 @@ -185,10 +185,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) } 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 834b80489b..2edd14b160 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 @@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline sealed interface 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 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 8b8f73eb1a..570c614603 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 @@ -75,10 +75,6 @@ class TimelinePresenter @AssistedInject constructor( @Composable override fun present(): TimelineState { val localScope = rememberCoroutineScope() - val highlightedEventId: MutableState = rememberSaveable { - mutableStateOf(null) - } - val focusedEventId: MutableState = rememberSaveable { mutableStateOf(null) } @@ -110,7 +106,6 @@ class TimelinePresenter @AssistedInject constructor( timelineController.paginate(direction = event.direction) } } - is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId is TimelineEvents.OnScrollFinished -> { if (isLive) { if (event.firstIndex == 0) { @@ -202,7 +197,6 @@ class TimelinePresenter @AssistedInject constructor( } return TimelineState( timelineRoomInfo = timelineRoomInfo, - highlightedEventId = highlightedEventId.value, timelineItems = timelineItems, renderReadReceipts = renderReadReceipts, newEventState = newEventState.value, 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 c7a077a721..7fb6c1ffdc 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 @@ -27,7 +27,6 @@ data class TimelineState( val timelineItems: ImmutableList, val timelineRoomInfo: TimelineRoomInfo, val renderReadReceipts: Boolean, - val highlightedEventId: EventId?, val newEventState: NewEventState, val isLive: Boolean, val focusedEventId : EventId?, 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 201c12837a..2d92027dcd 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 @@ -53,7 +53,6 @@ fun aTimelineState( timelineItems = timelineItems, timelineRoomInfo = timelineRoomInfo, renderReadReceipts = renderReadReceipts, - highlightedEventId = null, newEventState = NewEventState.None, isLive = true, focusedEventId = null, 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 e3dfa248ad..63a1cb1505 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 @@ -146,7 +146,7 @@ fun TimelineView( renderReadReceipts = state.renderReadReceipts, isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && state.timelineItems.first().identifier() == timelineItem.identifier(), - highlightedItem = state.highlightedEventId?.value, + highlightedItem = state.focusedEventId?.value, onClick = onMessageClicked, onLongClick = onMessageLongClicked, onUserDataClick = onUserDataClicked, From 1c06db1a7831df0830e9568d6b5d1f6f0e54e166 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Apr 2024 16:58:23 +0200 Subject: [PATCH 07/53] Timeline permalink : add focused event modifier --- .../messages/impl/timeline/TimelineView.kt | 2 +- .../TimelineItemGroupedEventsRow.kt | 14 +- .../timeline/components/TimelineItemRow.kt | 135 +++++++++++------- .../impl/timeline/model/TimelineItem.kt | 8 ++ .../designsystem/theme/ColorAliases.kt | 5 + 5 files changed, 104 insertions(+), 60 deletions(-) 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 63a1cb1505..477314e20e 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 @@ -146,7 +146,7 @@ fun TimelineView( renderReadReceipts = state.renderReadReceipts, isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && state.timelineItems.first().identifier() == timelineItem.identifier(), - highlightedItem = state.focusedEventId?.value, + focusedEventId = state.focusedEventId, onClick = onMessageClicked, onLongClick = onMessageLongClicked, onUserDataClick = onUserDataClicked, 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..35571278bb 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, @@ -165,7 +165,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi onExpandGroupClick = {}, timelineItem = aGroupedEvents(withReadReceipts = true), timelineRoomInfo = aTimelineRoomInfo(), - highlightedItem = null, + focusedEventId = null, renderReadReceipts = true, isLastOutgoingMessage = false, onClick = {}, @@ -190,7 +190,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 7c3557b774..4782ff21f1 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,23 @@ 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 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 +42,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,71 +57,92 @@ internal fun TimelineItemRow( eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier ) { - when (timelineItem) { - is TimelineItem.Virtual -> { - TimelineItemVirtualRow( - virtual = timelineItem, - timelineRoomInfo = timelineRoomInfo, - eventSink = eventSink, - modifier = modifier, - ) - } - 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) }, + + val backgroundModifier = if (timelineItem.isEvent(focusedEventId)) { + Modifier.focusedEvent() + } 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, - ) - } } } + +@Composable +private fun Modifier.focusedEvent(): Modifier { + val highlightedLineColor = ElementTheme.colors.textActionAccent + val gradientColors = listOf( + ElementTheme.colors.highlightedMessageBackgroundColor, + ElementTheme.materialColors.background + ) + val verticalOffset = 2.dp.toPx() + return drawWithCache { + val brush = Brush.verticalGradient(gradientColors) + onDrawBehind { + drawRect(brush, topLeft = Offset(0f, verticalOffset), size = Size(size.width, size.height * 0.7f)) + 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/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index fa00c26760..3f46639c6a 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 @@ -38,6 +38,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/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..1fedcceedd 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,11 @@ 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 { From 9b83cedac4b92c974393d25be5acdc803581750c Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Apr 2024 18:02:12 +0200 Subject: [PATCH 08/53] Timeline permalink : branch permalink on event of other room --- .../android/appnav/LoggedInFlowNode.kt | 2 +- .../android/appnav/room/RoomFlowNode.kt | 7 ++++-- .../appnav/room/joined/JoinedRoomFlowNode.kt | 7 ++++-- .../room/joined/JoinedRoomLoadedFlowNode.kt | 2 +- .../impl/DefaultMessagesEntryPoint.kt | 2 +- .../messages/impl/MessagesFlowNode.kt | 14 +++++------ .../features/messages/impl/MessagesNode.kt | 23 +++++++++++++++++-- 7 files changed, 41 insertions(+), 16 deletions(-) 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 6d6405c0e8..f397713d08 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 @@ -80,7 +80,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() @@ -166,7 +166,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/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..c77907a855 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 70e077eb04..634aca1733 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 @@ -37,6 +42,7 @@ 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 @@ -69,9 +75,10 @@ class MessagesNode @AssistedInject constructor( 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 @@ -87,7 +94,8 @@ class MessagesNode @AssistedInject constructor( fun onJoinCallClicked(roomId: RoomId) } - init { + override fun onBuilt() { + super.onBuilt() lifecycle.subscribe( onCreate = { analyticsService.capture(room.toAnalyticsViewRoom()) @@ -197,6 +205,17 @@ class MessagesNode @AssistedInject constructor( 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 + } } } } From 6af4bf30ffaae4c311f25dfad8466f48bba7eec5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 Apr 2024 12:34:52 +0200 Subject: [PATCH 09/53] Timeline : use latest api --- .../android/libraries/matrix/impl/room/RustMatrixRoom.kt | 8 ++++++-- .../matrix/impl/roomlist/RoomListItemExtensions.kt | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) 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 147ca3f2da..a6e51fefbc 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 @@ -184,8 +184,12 @@ class RustMatrixRoom( override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId) override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline { - return innerRoom.timelineFocusedOnEvent(eventId.value, numContextEvents = 50u).let {inner -> - createTimeline(inner, isLive = false){} + return innerRoom.timelineFocusedOnEvent( + eventId = eventId.value, + numContextEvents = 50u, + internalIdPrefix = "focus_$eventId", + ).let {inner -> + createTimeline(inner, isLive = false) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListItemExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListItemExtensions.kt index 24f292b456..1ab7d9f7d7 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListItemExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListItemExtensions.kt @@ -23,7 +23,7 @@ import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter /** Returns a `Room` with an initialized timeline using the given [filter]. */ suspend fun RoomListItem.fullRoomWithTimeline(filter: TimelineEventTypeFilter? = null): Room { if (!isTimelineInitialized()) { - initTimeline(filter) + initTimeline(filter, "live") } return fullRoom() } From b40f01a634c850bf0d42cd424fb147bfd226a8e3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 Apr 2024 13:37:14 +0200 Subject: [PATCH 10/53] Timeline : exposes same methods as the rust type and use them by default on liveTimeline --- .../libraries/matrix/api/timeline/Timeline.kt | 127 ++++++++ .../matrix/impl/room/RustMatrixRoom.kt | 221 +++---------- .../matrix/impl/timeline/RustTimeline.kt | 297 +++++++++++++++++- 3 files changed, 457 insertions(+), 188 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 05b1549ae1..c15c4b323f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -17,8 +17,20 @@ 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 { @@ -39,4 +51,119 @@ interface Timeline : AutoCloseable { 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/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 a6e51fefbc..c99c9a8c21 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 @@ -43,17 +43,12 @@ 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.Timeline 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 @@ -80,8 +75,6 @@ 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 @@ -94,10 +87,8 @@ 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 @@ -198,7 +189,6 @@ class RustMatrixRoom( liveTimeline.close() innerRoom.destroy() roomListItem.destroy() - specialModeEventTimelineItem?.destroy() } override val name: String? @@ -332,12 +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 sendMessage(body: String, htmlBody: String?, mentions: List): Result { + return liveTimeline.sendMessage(body, htmlBody, mentions) } override suspend fun editMessage( @@ -346,45 +332,16 @@ class RustMatrixRoom( 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) } - } + ): Result { + return liveTimeline.editMessage(originalEventId, transactionId, body, htmlBody, mentions) } - 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 enterSpecialMode(eventId: EventId?): Result { + return liveTimeline.enterSpecialMode(eventId) + } + + override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result{ + return liveTimeline.replyMessage(eventId, body, htmlBody, mentions) } override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) { @@ -467,18 +424,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( @@ -489,63 +435,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) { @@ -623,16 +537,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( @@ -640,15 +546,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( @@ -657,46 +556,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( @@ -704,16 +579,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 { @@ -749,12 +616,6 @@ class RustMatrixRoom( innerRoom.matrixToEventPermalink(eventId.value) } - private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { - return runCatching { - MediaUploadHandlerImpl(files, handle()) - } - } - private fun createTimeline( timeline: InnerTimeline, isLive: Boolean = true, @@ -769,19 +630,9 @@ class RustMatrixRoom( dispatcher = roomDispatcher, lastLoginTimestamp = sessionData.loginTimestamp, onNewSyncedEvent = onNewSyncedEvent, + roomContentForwarder = roomContentForwarder, inner = timeline, - fetchDetailsForEvent = { eventId -> - runCatching { - innerTimeline.getEventTimelineItemByEventId(eventId.value) - } - } ) } - 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/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index a3c121fd97..43c53dcf0d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -17,11 +17,30 @@ 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 @@ -48,10 +67,19 @@ 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 @@ -68,7 +96,7 @@ class RustTimeline( private val matrixRoom: MatrixRoom, private val dispatcher: CoroutineDispatcher, private val lastLoginTimestamp: Date?, - private val fetchDetailsForEvent: suspend (EventId) -> Result, + private val roomContentForwarder: RoomContentForwarder, private val onNewSyncedEvent: () -> Unit, ) : Timeline { @@ -90,7 +118,7 @@ class RustTimeline( private val invisibleIndicatorPostProcessor = InvisibleIndicatorPostProcessor(isLive) private val timelineItemFactory = MatrixTimelineItemMapper( - fetchDetailsForEvent = fetchDetailsForEvent, + fetchDetailsForEvent = this::fetchDetailsForEvent, roomCoroutineScope = roomCoroutineScope, virtualTimelineItemMapper = VirtualTimelineItemMapper(), eventTimelineItemMapper = EventTimelineItemMapper( @@ -138,7 +166,7 @@ class RustTimeline( } } - private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus)->Timeline.PaginationStatus){ + 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) @@ -204,6 +232,7 @@ class RustTimeline( override fun close() { inner.close() + specialModeEventTimelineItem?.destroy() } private suspend fun fetchMembers() = withContext(dispatcher) { @@ -229,4 +258,266 @@ class RustTimeline( 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) } + } + } + + 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.getEventTimelineItemByEventId(eventId.value) + } + } } From bb0ba5c4bfda3b25d9848239d519768d34325859 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 Apr 2024 16:42:35 +0200 Subject: [PATCH 11/53] Timeline : makes sure to use the right timeline when making some action (edit, reply, reaction) --- .../features/messages/impl/MessagesNode.kt | 3 ++ .../messages/impl/MessagesPresenter.kt | 8 +++- .../MessageComposerPresenter.kt | 21 ++++++---- .../impl/timeline/TimelineController.kt | 26 ++++++++++-- .../features/poll/impl/data/PollRepository.kt | 21 ++++++---- .../libraries/matrix/api/room/MatrixRoom.kt | 1 + .../matrix/api/timeline/TimelineProvider.kt | 41 +++++++++++++++++++ 7 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt 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 634aca1733..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 @@ -35,6 +35,7 @@ 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 @@ -71,6 +72,7 @@ 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() @@ -101,6 +103,7 @@ class MessagesNode @AssistedInject constructor( analyticsService.capture(room.toAnalyticsViewRoom()) }, onDestroy = { + timelineController.close() mediaPlayer.close() } ) 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 6531c0f8d5..db46bc0c86 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) @@ -286,8 +288,10 @@ class MessagesPresenter @AssistedInject constructor( emoji: String, eventId: EventId, ) = launch(dispatchers.io) { - room.toggleReaction(emoji, eventId) - .onFailure { Timber.e(it) } + timelineController.invokeOnTimeline { + 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/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 04c182a566..f7072922c7 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.invokeOnTimeline { + 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.invokeOnTimeline { + 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.invokeOnTimeline { + 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 index 9f98614fac..811ad18075 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -17,11 +17,14 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.MutableState +import com.squareup.anvil.annotations.ContributesBinding 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 io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider +import io.element.android.libraries.matrix.api.timeline.TimelineProvider 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 @@ -30,23 +33,25 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.map +import java.io.Closeable import java.util.Optional import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException /** - * This controller is responsible of using the right timeline to display messages. + * 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). + * This controller will replace the [LiveTimelineProvider] in the DI. */ @SingleIn(RoomScope::class) +@ContributesBinding(RoomScope::class, boundType = TimelineProvider::class, replaces = [LiveTimelineProvider::class]) class TimelineController @Inject constructor( private val room: MatrixRoom, -) { +) : Closeable, TimelineProvider { private val liveTimeline = MutableStateFlow(room.liveTimeline) private val detachedTimeline = MutableStateFlow>(Optional.empty()) @@ -60,6 +65,12 @@ class TimelineController @Inject constructor( return detachedTimeline.map { !it.isPresent } } + suspend fun invokeOnTimeline(block: suspend (Timeline.() -> Any)) { + currentTimelineFlow().first().run { + block(this) + } + } + suspend fun focusOnEvent(eventId: EventId): Result { return try { val newDetachedTimeline = room.timelineFocusedOnEvent(eventId) @@ -93,6 +104,10 @@ class TimelineController @Inject constructor( } } + override fun close() { + focusOnLive() + } + suspend fun paginate(direction: Timeline.PaginationDirection): Result { return currentTimelineFlow().first().paginate(direction) .onSuccess { hasReachedEnd -> @@ -124,7 +139,6 @@ class TimelineController @Inject constructor( if (eventId != null && eventId != lastReadReceiptId.value) { lastReadReceiptId.value = eventId currentTimelineFlow() - .filterIsInstance(Timeline::class) .first() .sendReadReceipt(eventId = eventId, receiptType = readReceiptType) } @@ -140,4 +154,8 @@ class TimelineController @Inject constructor( } return null } + + override suspend fun getActiveTimeline(): Timeline { + return currentTimelineFlow().first() + } } 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 952fe97a5a..55d2bd8099 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,18 @@ 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.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.liveTimeline + timelineProvider + .getActiveTimeline() .timelineItems .first() .asSequence() @@ -51,13 +54,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/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 a2ca077bb8..1e9b926aaf 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,6 +32,7 @@ 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.TimelineProvider import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver 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..242859fede --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt @@ -0,0 +1,41 @@ +/* + * 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 com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.MatrixRoom +import javax.inject.Inject + +/** + * 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 { + suspend fun getActiveTimeline(): Timeline +} + +/** + * Default implementation of [TimelineProvider] that provides the live timeline of a room. + */ +@ContributesBinding(RoomScope::class) +class LiveTimelineProvider @Inject constructor( + private val room: MatrixRoom, +) : TimelineProvider { + override suspend fun getActiveTimeline(): Timeline = room.liveTimeline +} + From 60192bd08024b2dd15613924797e3cbdcc330119 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 Apr 2024 21:33:20 +0200 Subject: [PATCH 12/53] Timeline permalink : start updating tests --- .../messages/impl/MessagesPresenter.kt | 2 +- .../impl/forward/ForwardMessagesPresenter.kt | 5 +- .../MessageComposerPresenter.kt | 6 +- .../impl/timeline/TimelineController.kt | 37 +-- .../impl/timeline/TimelinePresenter.kt | 3 +- .../messages/impl/timeline/TimelineView.kt | 4 - .../factories/TimelineItemsFactory.kt | 4 - .../messages/impl/MessagesPresenterTest.kt | 6 + .../fixtures/TimelineItemsFactoryFixtures.kt | 6 +- .../forward/ForwardMessagesPresenterTests.kt | 3 +- .../MessageComposerPresenterTest.kt | 2 + .../impl/timeline/TimelinePresenterTest.kt | 287 ++++++++++-------- .../impl/timeline/TimelineViewTest.kt | 18 +- .../features/poll/impl/PollFixtures.kt | 6 +- .../impl/create/CreatePollPresenterTest.kt | 4 +- .../impl/history/PollHistoryPresenterTest.kt | 2 +- .../matrix/test/room/FakeMatrixRoom.kt | 9 +- .../test/timeline/FakeMatrixTimeline.kt | 96 ------ .../matrix/test/timeline/FakeTimeline.kt | 205 +++++++++++++ 19 files changed, 410 insertions(+), 295 deletions(-) delete mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt 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 db46bc0c86..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 @@ -288,7 +288,7 @@ class MessagesPresenter @AssistedInject constructor( emoji: String, eventId: EventId, ) = launch(dispatchers.io) { - timelineController.invokeOnTimeline { + timelineController.invokeOnCurrentTimeline { toggleReaction(emoji, eventId) .onFailure { Timber.e(it) } } 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..fd26109e0a 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 @@ -30,6 +30,7 @@ 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 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 f7072922c7..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 @@ -266,7 +266,7 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerMode.Quote -> null }.let { relatedEventId -> appCoroutineScope.launch { - timelineController.invokeOnTimeline { + timelineController.invokeOnCurrentTimeline { enterSpecialMode(relatedEventId) } } @@ -390,14 +390,14 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerMode.Edit -> { val eventId = capturedMode.eventId val transactionId = capturedMode.transactionId - timelineController.invokeOnTimeline { + timelineController.invokeOnCurrentTimeline { editMessage(eventId, transactionId, message.markdown, message.html, mentions) } } is MessageComposerMode.Quote -> TODO() is MessageComposerMode.Reply -> { - timelineController.invokeOnTimeline { + timelineController.invokeOnCurrentTimeline { replyMessage(capturedMode.eventId, message.markdown, message.html, mentions) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index 811ad18075..ce6bdfc7d3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -16,9 +16,7 @@ package io.element.android.features.messages.impl.timeline -import androidx.compose.runtime.MutableState import com.squareup.anvil.annotations.ContributesBinding -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 @@ -26,9 +24,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider import io.element.android.libraries.matrix.api.timeline.TimelineProvider 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 kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -65,7 +61,7 @@ class TimelineController @Inject constructor( return detachedTimeline.map { !it.isPresent } } - suspend fun invokeOnTimeline(block: suspend (Timeline.() -> Any)) { + suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) { currentTimelineFlow().first().run { block(this) } @@ -124,37 +120,6 @@ class TimelineController @Inject constructor( } } - suspend fun sendReadReceiptIfNeeded( - firstVisibleIndex: Int, - timelineItems: ImmutableList, - lastReadReceiptId: MutableState, - readReceiptType: ReceiptType, - ) { - // If we are at the bottom of timeline, we mark the room as read. - if (firstVisibleIndex == 0) { - room.markAsRead(receiptType = readReceiptType) - } else { - // Get last valid EventId seen by the user, as the first index might refer to a Virtual item - val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) - if (eventId != null && eventId != lastReadReceiptId.value) { - lastReadReceiptId.value = eventId - currentTimelineFlow() - .first() - .sendReadReceipt(eventId = eventId, receiptType = readReceiptType) - } - } - } - - private fun getLastEventIdBeforeOrAt(index: Int, items: ImmutableList): EventId? { - for (i in index until items.count()) { - val item = items[i] - if (item is TimelineItem.Event) { - return item.eventId - } - } - return null - } - override suspend fun getActiveTimeline(): Timeline { return currentTimelineFlow().first() } 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 570c614603..f3ba877888 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 @@ -111,6 +111,7 @@ class TimelinePresenter @AssistedInject constructor( if (event.firstIndex == 0) { newEventState.value = NewEventState.None } + println("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}") appScope.sendReadReceiptIfNeeded( firstVisibleIndex = event.firstIndex, timelineItems = timelineItems, @@ -256,7 +257,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) } } } 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 477314e20e..f15a0807cd 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 @@ -116,10 +116,6 @@ fun TimelineView( // TODO implement this logic once we have support to 'jump to event X' in sliding sync } - LaunchedEffect(key1 = state.timelineItems) { - Timber.d("TimelineView - timelineItem identifiers: ${state.timelineItems.joinToString(", ") { it.identifier() }}") - } - // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms AnimatedVisibility(visible = true, enter = fadeIn()) { Box(modifier) { 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 34049fbaf7..ac9803506c 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 @@ -62,10 +62,6 @@ class TimelineItemsFactory @Inject constructor( } } - fun items(): StateFlow> { - return timelineItems - } - @Composable fun collectItemsAsState(): State> { return timelineItems.collectAsState() 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..78dc7612a8 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 @@ -748,6 +750,7 @@ class MessagesPresenterTest { currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), permalinkParser = FakePermalinkParser(), permalinkBuilder = FakePermalinkBuilder(), + timelineController = TimelineController(matrixRoom), ) val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( this, @@ -768,6 +771,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 +809,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..d1b054faf8 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,6 +21,7 @@ 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.timeline.LiveTimelineProvider 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 @@ -91,7 +92,7 @@ class ForwardMessagesPresenterTests { 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..b099562480 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 @@ -968,6 +969,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/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 8d5699d4e0..3dd00021fe 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 @@ -35,9 +35,11 @@ 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.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 @@ -47,20 +49,28 @@ 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.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -71,7 +81,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() @@ -83,58 +93,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.backPaginationStatus.hasMoreToLoadBackwards).isTrue() - assertThat(initialState.backPaginationStatus.isBackPaginating).isFalse() - initialState.eventSink.invoke(TimelineEvents.LoadMore) - val inPaginationState = awaitItem() - assertThat(inPaginationState.backPaginationStatus.isBackPaginating).isTrue() - assertThat(inPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue() - val postPaginationState = awaitItem() - assertThat(postPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue() - assertThat(postPaginationState.backPaginationStatus.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, @@ -143,7 +144,6 @@ class TimelinePresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sentReadReceipts).isEmpty() val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) runCurrent() @@ -154,48 +154,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, @@ -204,75 +218,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() @@ -280,12 +305,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)) } @@ -294,7 +319,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)) } @@ -306,7 +331,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)) } @@ -320,7 +345,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() @@ -348,10 +376,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 @@ -423,8 +450,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, ) @@ -432,32 +461,32 @@ 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 - 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) } @@ -484,16 +513,12 @@ 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(), @@ -511,6 +536,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 be7df31635..0f99e8b558 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 @@ -19,10 +19,14 @@ package io.element.android.features.messages.impl.timeline import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule 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.aTypingNotificationState +import io.element.android.libraries.matrix.api.timeline.Timeline 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.runner.RunWith @@ -37,10 +41,13 @@ class TimelineViewTest { rule.setContent { TimelineView( aTimelineState( + timelineItems = persistentListOf( + TimelineItem.Virtual( + id = "backward_pagination", + model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0) + ), + ), eventSink = eventsRecorder, - paginationState = aPaginationState( - hasMoreToLoadBackwards = true, - ) ), typingNotificationState = aTypingNotificationState(), onUserDataClicked = EnsureNeverCalledWithParam(), @@ -55,7 +62,7 @@ class TimelineViewTest { onReadReceiptClick = EnsureNeverCalledWithParam(), ) } - eventsRecorder.assertSingle(TimelineEvents.LoadMore) + eventsRecorder.assertSingle(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)) } @Test @@ -65,9 +72,6 @@ class TimelineViewTest { TimelineView( aTimelineState( eventSink = eventsRecorder, - paginationState = aPaginationState( - hasMoreToLoadBackwards = false, - ) ), typingNotificationState = aTypingNotificationState(), onUserDataClicked = EnsureNeverCalledWithParam(), 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..84ef0aae31 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,15 +20,15 @@ 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.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import kotlinx.collections.immutable.persistentListOf fun aPollTimeline( polls: Map = emptyMap(), -): FakeMatrixTimeline { - return FakeMatrixTimeline( +): FakeTimeline { + return FakeTimeline( initialTimelineItems = polls.map { entry -> MatrixTimelineItem.Event( entry.key.value, 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..445187168e 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 @@ -49,7 +49,7 @@ class CreatePollPresenterTest { private var navUpInvocationsCount = 0 private val existingPoll = anOngoingPollContent() private val fakeMatrixRoom = FakeMatrixRoom( - matrixTimeline = aPollTimeline( + liveTimeline = aPollTimeline( mapOf(pollEventId to existingPoll) ) ) @@ -80,7 +80,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 = aPollTimeline() ) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room) moleculeFlow(RecompositionMode.Immediate) { 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..c34c9074f0 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 @@ -57,7 +57,7 @@ class PollHistoryPresenterTest { ) ) private val room = FakeMatrixRoom( - matrixTimeline = timeline + liveTimeline = timeline ) @Test 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 96bcd3330d..17f9358df7 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 @@ -44,6 +44,7 @@ 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.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 @@ -53,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 @@ -83,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, @@ -214,7 +215,9 @@ class FakeMatrixRoom( override val syncUpdateFlow: StateFlow = MutableStateFlow(0L) - override val timeline: MatrixTimeline = matrixTimeline + override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline { + return FakeTimeline() + } override suspend fun subscribeToSync() = Unit 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 f98e8d0fcb..0000000000 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt +++ /dev/null @@ -1,96 +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.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..5663ad6b5b --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -0,0 +1,205 @@ +/* + * 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 io.element.android.tests.testutils.simulateLongTask +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( + 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 + } + } + + override fun close() = Unit +} From 9ffed343032d83df498b6689072e24df23d4a560 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Apr 2024 12:41:34 +0200 Subject: [PATCH 13/53] Timeline : continue to fix more tests... --- .../appnav/JoinRoomLoadedFlowNodeTest.kt | 5 +- .../actions/AndroidLocationActionsTest.kt | 2 + .../messages/impl/MessagesPresenterTest.kt | 43 ++++++++--- .../MessageComposerPresenterTest.kt | 74 ++++++++++++++++--- .../features/poll/impl/PollFixtures.kt | 11 +-- .../impl/create/CreatePollPresenterTest.kt | 71 ++++++++++++------ .../impl/history/PollHistoryPresenterTest.kt | 36 ++++++--- .../android/samples/minimal/RoomListScreen.kt | 3 +- .../tests/testutils/lambda/LambdaRecorder.kt | 16 ++++ 9 files changed, 198 insertions(+), 63 deletions(-) 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/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt index 6cd7cf82ce..9665c886d7 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt @@ -18,9 +18,11 @@ package io.element.android.features.location.impl.common.actions import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location +import org.junit.Ignore import org.junit.Test import java.net.URLEncoder +@Ignore internal class AndroidLocationActionsTest { // We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII") 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 78dc7612a8..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 @@ -65,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 @@ -83,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 @@ -97,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 @@ -169,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() @@ -177,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)), + ) } } @@ -274,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) @@ -430,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 } } 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 b099562480..c30d427239 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 @@ -66,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 @@ -82,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 @@ -260,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, @@ -284,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, @@ -298,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, @@ -322,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, @@ -336,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, @@ -356,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, @@ -832,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() @@ -867,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) @@ -883,7 +936,10 @@ 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) } 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 84ef0aae31..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.FakeTimeline 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(), -): FakeTimeline { - return FakeTimeline( - 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 445187168e..0b33b7762f 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.LiveTimelineProvider 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.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( - liveTimeline = 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( - liveTimeline = 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 c34c9074f0..9a4af79dd6 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,20 @@ 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.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,11 +56,14 @@ class PollHistoryPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val timeline = aPollTimeline( - polls = mapOf( + 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( liveTimeline = timeline @@ -66,7 +75,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 +135,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) } } 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 49fa29ced3..af3f0aa820 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 @@ -146,7 +147,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) + } +} From cf1c728eabb020c2a58f1738be2eeb67f339022f Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Apr 2024 14:35:37 +0200 Subject: [PATCH 14/53] Timeline : makes sure all tests are passing --- .../impl/forward/ForwardMessagesPresenter.kt | 1 + .../impl/timeline/TimelineController.kt | 32 +++++++++++++------ .../messages/impl/timeline/TimelineState.kt | 4 ++- .../messages/impl/timeline/TimelineView.kt | 20 +++++------- .../forward/ForwardMessagesPresenterTests.kt | 32 +++++++++++++++---- .../features/poll/impl/data/PollRepository.kt | 1 + .../poll/impl/history/PollHistoryPresenter.kt | 7 ++-- .../impl/history/PollHistoryPresenterTest.kt | 3 +- .../matrix/api/timeline/TimelineProvider.kt | 11 +++++-- .../HasEncryptionHistoryBanner.kt | 2 +- .../RoomBeginningPostProcessor.kt | 15 ++++++--- ...t.kt => RoomBeginningPostProcessorTest.kt} | 29 ++++++++++++----- 12 files changed, 108 insertions(+), 49 deletions(-) rename libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/{DmBeginningTimelineProcessorTest.kt => RoomBeginningPostProcessorTest.kt} (86%) 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 fd26109e0a..a2212b69f3 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 @@ -31,6 +31,7 @@ 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 diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index ce6bdfc7d3..c4d794b7c4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -22,17 +22,22 @@ 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.LiveTimelineProvider -import io.element.android.libraries.matrix.api.timeline.TimelineProvider 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.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.first import kotlinx.coroutines.flow.flatMapLatest 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 @@ -49,12 +54,14 @@ class TimelineController @Inject constructor( private val room: MatrixRoom, ) : Closeable, TimelineProvider { + private val coroutineScope = CoroutineScope(SupervisorJob()) + private val liveTimeline = MutableStateFlow(room.liveTimeline) private val detachedTimeline = MutableStateFlow>(Optional.empty()) @OptIn(ExperimentalCoroutinesApi::class) fun timelineItems(): Flow> { - return currentTimelineFlow().flatMapLatest { it.timelineItems } + return currentTimelineFlow.flatMapLatest { it.timelineItems } } fun isLive(): Flow { @@ -62,7 +69,7 @@ class TimelineController @Inject constructor( } suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) { - currentTimelineFlow().first().run { + currentTimelineFlow.value.run { block(this) } } @@ -89,6 +96,10 @@ class TimelineController @Inject constructor( * This does close the detached timeline if any. */ fun focusOnLive() { + closeDetachedTimeline() + } + + private fun closeDetachedTimeline() { detachedTimeline.getAndUpdate { when { it.isPresent -> { @@ -101,11 +112,12 @@ class TimelineController @Inject constructor( } override fun close() { - focusOnLive() + coroutineScope.cancel() + closeDetachedTimeline() } suspend fun paginate(direction: Timeline.PaginationDirection): Result { - return currentTimelineFlow().first().paginate(direction) + return currentTimelineFlow.value.paginate(direction) .onSuccess { hasReachedEnd -> if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) { focusOnLive() @@ -113,14 +125,14 @@ class TimelineController @Inject constructor( } } - private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached -> + private val currentTimelineFlow = combine(liveTimeline, detachedTimeline) { live, detached -> when { detached.isPresent -> detached.get() else -> live } - } + }.stateIn(coroutineScope, SharingStarted.Eagerly, room.liveTimeline) - override suspend fun getActiveTimeline(): Timeline { - return currentTimelineFlow().first() + override fun activeTimelineFlow(): StateFlow { + return currentTimelineFlow } } 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 7fb6c1ffdc..726870a5b7 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 @@ -32,7 +32,9 @@ data class TimelineState( val focusedEventId : EventId?, val focusRequestState: FocusRequestState, val eventSink: (TimelineEvents) -> Unit, -) +){ + val isTimelineEmpty = timelineItems.none { it is TimelineItem.Event } +} sealed interface FocusRequestState { data object None : FocusRequestState 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 f15a0807cd..1e1c04a677 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 @@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.typing.TypingNotificationState +import io.element.android.features.messages.impl.typing.TypingNotificationView import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog @@ -125,12 +126,12 @@ fun TimelineView( reverseLayout = useReverseLayout, contentPadding = PaddingValues(vertical = 8.dp), ) { - /* - item(key = UUID.randomUUID()) { - TypingNotificationView(state = typingNotificationState) - } - */ + if(state.isLive) { + item { + TypingNotificationView(state = typingNotificationState) + } + } items( items = state.timelineItems, contentType = { timelineItem -> timelineItem.contentType() }, @@ -165,7 +166,7 @@ fun TimelineView( ) TimelineScrollHelper( - isTimelineEmpty = state.timelineItems.isEmpty(), + isTimelineEmpty = state.isTimelineEmpty, lazyListState = lazyListState, forceJumpToBottomVisibility = forceJumpToBottomVisibility, newEventState = state.newEventState, @@ -185,10 +186,6 @@ private fun FocusRequestStateView( onClearFocusRequestState: () -> Unit, modifier: Modifier = Modifier, ) { - BackHandler(enabled = focusRequestState is FocusRequestState.Fetching) { - onClearFocusRequestState() - } - when (focusRequestState) { is FocusRequestState.Failure -> { ErrorDialog( @@ -198,7 +195,7 @@ private fun FocusRequestStateView( ) } FocusRequestState.Fetching -> { - ProgressDialog(modifier = modifier) + ProgressDialog(modifier = modifier, onDismissRequest = onClearFocusRequestState) } is FocusRequestState.Cached, FocusRequestState.None -> Unit } @@ -254,7 +251,6 @@ private fun BoxScope.TimelineScrollHelper( } LaunchedEffect(canAutoScroll, newEventState) { - Timber.d("TimelineScrollHelper - canAutoScroll: $canAutoScroll, newEventState: $newEventState") val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe) if (shouldScrollToBottom) { scrollToBottom() 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 d1b054faf8..f6c749ea32 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,15 +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.api.timeline.LiveTimelineProvider 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.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 @@ -37,7 +42,7 @@ class ForwardMessagesPresenterTests { @Test fun `present - initial state`() = runTest { - val presenter = aPresenter() + val presenter = aForwardMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -50,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 { @@ -62,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)) @@ -83,10 +100,11 @@ 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, 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 55d2bd8099..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 @@ -21,6 +21,7 @@ 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 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 b36359aa25..7a3b3f8232 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 @@ -34,22 +34,23 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItemsFacto import io.element.android.libraries.architecture.Presenter 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.CoroutineScope +import kotlinx.coroutines.flow.flatMapConcat 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.liveTimeline + val timeline by timelineProvider.activeTimelineFlow().collectAsState() val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState() val pollHistoryItemsFlow = remember { timeline.timelineItems.map { items -> 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 9a4af79dd6..07f71f51d3 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 @@ -32,6 +32,7 @@ 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.LiveTimelineProvider 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 @@ -174,11 +175,11 @@ class PollHistoryPresenterTest { ), ): PollHistoryPresenter { return PollHistoryPresenter( - room = room, appCoroutineScope = appCoroutineScope, sendPollResponseAction = sendPollResponseAction, endPollAction = endPollAction, pollHistoryItemFactory = pollHistoryItemFactory, + timelineProvider = LiveTimelineProvider(room), ) } } 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 index 242859fede..5b55b8dc47 100644 --- 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 @@ -19,6 +19,11 @@ package io.element.android.libraries.matrix.api.timeline import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import javax.inject.Inject /** @@ -26,9 +31,11 @@ import javax.inject.Inject * It could be the current room timeline, or a timeline for a specific event. */ interface TimelineProvider { - suspend fun getActiveTimeline(): Timeline + fun activeTimelineFlow(): StateFlow } +suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().first() + /** * Default implementation of [TimelineProvider] that provides the live timeline of a room. */ @@ -36,6 +43,6 @@ interface TimelineProvider { class LiveTimelineProvider @Inject constructor( private val room: MatrixRoom, ) : TimelineProvider { - override suspend fun getActiveTimeline(): Timeline = room.liveTimeline + override fun activeTimelineFlow(): StateFlow = MutableStateFlow(room.liveTimeline) } 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 index 882d89bbee..2d6801918e 100644 --- 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 @@ -19,7 +19,7 @@ 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 -fun List.hasEncryptionHistoryBanner(): Boolean { +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/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index fed2266659..da8392a452 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -16,6 +16,7 @@ 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 @@ -43,10 +44,7 @@ class RoomBeginningPostProcessor { private fun processForRoom(items: List): List { if (items.hasEncryptionHistoryBanner()) return items - val roomBeginningItem = MatrixTimelineItem.Virtual( - uniqueId = VirtualTimelineItem.RoomBeginning.toString(), - virtual = VirtualTimelineItem.RoomBeginning - ) + val roomBeginningItem = createRoomBeginningItem() return listOf(roomBeginningItem) + items } @@ -77,4 +75,13 @@ class RoomBeginningPostProcessor { } 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 86% 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 40a15f7621..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,13 +22,14 @@ 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( @@ -36,7 +37,7 @@ class DmBeginningTimelineProcessorTest { MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), ) val processor = RoomBeginningPostProcessor() - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) assertThat(processedItems).isEmpty() } @@ -53,18 +54,30 @@ class DmBeginningTimelineProcessorTest { MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))), ) val processor = RoomBeginningPostProcessor() - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) + 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 = RoomBeginningPostProcessor() - val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = true) + 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) } @@ -75,7 +88,7 @@ class DmBeginningTimelineProcessorTest { MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), ) val processor = RoomBeginningPostProcessor() - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -85,7 +98,7 @@ class DmBeginningTimelineProcessorTest { MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), ) val processor = RoomBeginningPostProcessor() - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -96,7 +109,7 @@ class DmBeginningTimelineProcessorTest { MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))), ) val processor = RoomBeginningPostProcessor() - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) + val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } } From 7b07adc5ab33ec164d940bf379bb75dae698d731 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Apr 2024 15:01:18 +0200 Subject: [PATCH 15/53] Timeline : do not cancel pagination to avoid stuck timeline. --- .../libraries/matrix/impl/timeline/RustTimeline.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 43c53dcf0d..1e572d925d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -54,6 +54,7 @@ import kotlinx.coroutines.CancellationException 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 @@ -173,9 +174,10 @@ class RustTimeline( } } - override suspend fun paginate(direction: Timeline.PaginationDirection): Result { + // Use NonCancellable to avoid breaking the timeline when the coroutine is cancelled. + override suspend fun paginate(direction: Timeline.PaginationDirection): Result = withContext(NonCancellable) { initLatch.await() - return runCatching { + runCatching { if (!canPaginate(direction)) throw TimelineException.CannotPaginate updatePaginationStatus(direction) { it.copy(isPaginating = true) } when (direction) { @@ -184,9 +186,6 @@ class RustTimeline( } }.onFailure { error -> updatePaginationStatus(direction) { it.copy(isPaginating = false) } - if (error is CancellationException) { - throw error - } if (error is TimelineException.CannotPaginate) { Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}") } else { From b46f8a5c272d34b974b54e5a5a6995e2a0393728 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Apr 2024 15:13:19 +0200 Subject: [PATCH 16/53] Timeline : rename isTimelineEmpty to hasAnyEvent --- .../features/messages/impl/timeline/TimelineState.kt | 2 +- .../features/messages/impl/timeline/TimelineView.kt | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) 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 726870a5b7..4a345af90c 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 @@ -33,7 +33,7 @@ data class TimelineState( val focusRequestState: FocusRequestState, val eventSink: (TimelineEvents) -> Unit, ){ - val isTimelineEmpty = timelineItems.none { it is TimelineItem.Event } + val hasAnyEvent = timelineItems.any { it is TimelineItem.Event } } sealed interface FocusRequestState { 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 1e1c04a677..db7b89833b 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 @@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.timeline import android.view.accessibility.AccessibilityManager -import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween @@ -75,7 +74,6 @@ 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 timber.log.Timber import kotlin.math.abs @Composable @@ -166,7 +164,7 @@ fun TimelineView( ) TimelineScrollHelper( - isTimelineEmpty = state.isTimelineEmpty, + hasAnyEvent = state.hasAnyEvent, lazyListState = lazyListState, forceJumpToBottomVisibility = forceJumpToBottomVisibility, newEventState = state.newEventState, @@ -203,7 +201,7 @@ private fun FocusRequestStateView( @Composable private fun BoxScope.TimelineScrollHelper( - isTimelineEmpty: Boolean, + hasAnyEvent: Boolean, lazyListState: LazyListState, newEventState: NewEventState, isLive: Boolean, @@ -258,8 +256,8 @@ private fun BoxScope.TimelineScrollHelper( } 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) } From 82d833b4349b4f760d175b5286d9c76e0d69b4e4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Apr 2024 17:41:10 +0200 Subject: [PATCH 17/53] Timeline permalink : scroll to fetched event when known --- .../messages/impl/timeline/TimelinePresenter.kt | 17 +++++++++++------ .../messages/impl/timeline/TimelineState.kt | 1 + .../messages/impl/timeline/TimelineView.kt | 4 ++-- .../components/TimelineItemVirtualRow.kt | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) 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 f3ba877888..61b2add5cb 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 @@ -146,7 +146,7 @@ class TimelinePresenter @AssistedInject constructor( timelineController.focusOnEvent(event.eventId) .fold( onSuccess = { - focusRequestState.value = FocusRequestState.None + focusRequestState.value = FocusRequestState.Fetched }, onFailure = { focusRequestState.value = FocusRequestState.Failure(it) @@ -165,15 +165,20 @@ class TimelinePresenter @AssistedInject constructor( } } - // Makes sure to get back to live when there is nothing more to load forwards - LaunchedEffect(isLive) { - - } - LaunchedEffect(timelineItems.size) { 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(timelineController.timelineItems(), room.membersStateFlow) { items, membersState -> timelineItemsFactory.replaceWith( 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 4a345af90c..67a7dac574 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 @@ -40,6 +40,7 @@ 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 } 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 db7b89833b..101ad58703 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 @@ -195,7 +195,7 @@ private fun FocusRequestStateView( FocusRequestState.Fetching -> { ProgressDialog(modifier = modifier, onDismissRequest = onClearFocusRequestState) } - is FocusRequestState.Cached, FocusRequestState.None -> Unit + else -> Unit } } @@ -237,7 +237,7 @@ private fun BoxScope.TimelineScrollHelper( } } - LaunchedEffect(key1 = focusRequestState) { + LaunchedEffect(focusRequestState) { if (focusRequestState is FocusRequestState.Cached) { if (abs(lazyListState.firstVisibleItemIndex - focusRequestState.index) < 10) { lazyListState.animateScrollToItem(focusRequestState.index) 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 9dd1210af0..df9d355dbf 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 @@ -51,7 +51,7 @@ fun TimelineItemVirtualRow( TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name) is TimelineItemLoadingIndicatorModel -> { TimelineLoadingMoreIndicator() - LaunchedEffect(key1 = virtual.model.timestamp) { + LaunchedEffect(virtual.model.timestamp) { eventSink(TimelineEvents.LoadMore(virtual.model.direction)) } } From 345d4c7da31dbf6a99dcc3d14d17b9f2f2d801b2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Apr 2024 23:34:04 +0200 Subject: [PATCH 18/53] Timeline : try to get better forward pagination. --- .../components/TimelineItemVirtualRow.kt | 8 +- .../virtual/TimelineLoadingMoreIndicator.kt | 39 ++++++--- .../virtual/TimelineItemVirtualFactory.kt | 4 +- ... TimelineItemLastForwardIndicatorModel.kt} | 4 +- .../item/virtual/VirtualTimelineItem.kt | 2 +- .../matrix/impl/timeline/RustTimeline.kt | 8 +- .../InvisibleIndicatorPostProcessor.kt | 79 ------------------- .../LastForwardIndicatorsPostProcessor.kt | 76 ++++++++++++++++++ 8 files changed, 119 insertions(+), 101 deletions(-) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/{TimelineItemInvisibleIndicatorModel.kt => TimelineItemLastForwardIndicatorModel.kt} (81%) delete mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/InvisibleIndicatorPostProcessor.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt 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 df9d355dbf..635cda526e 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 @@ -31,7 +31,7 @@ import io.element.android.features.messages.impl.timeline.components.virtual.Tim 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.TimelineItemInvisibleIndicatorModel +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 @@ -50,12 +50,14 @@ fun TimelineItemVirtualRow( is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView() TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name) is TimelineItemLoadingIndicatorModel -> { - TimelineLoadingMoreIndicator() + TimelineLoadingMoreIndicator(virtual.model.direction) LaunchedEffect(virtual.model.timestamp) { eventSink(TimelineEvents.LoadMore(virtual.model.direction)) } } - TimelineItemInvisibleIndicatorModel -> Spacer(Modifier) + 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 cf5c54f3d4..90662957be 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 @@ -17,35 +17,54 @@ package io.element.android.features.messages.impl.timeline.components.virtual 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 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 = modifier - .fillMaxWidth() - .padding(2.dp), + modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { - LinearProgressIndicator(modifier = Modifier - .height(1.dp) - .fillMaxWidth() - ) + 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 { + TimelineLoadingMoreIndicator(Timeline.PaginationDirection.FORWARDS) + TimelineLoadingMoreIndicator(Timeline.PaginationDirection.BACKWARDS) + } } 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 e6df9e889d..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,7 @@ 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.TimelineItemInvisibleIndicatorModel +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 @@ -49,7 +49,7 @@ class TimelineItemVirtualFactory @Inject constructor( direction = inner.direction, timestamp = inner.timestamp ) - VirtualTimelineItem.LatestKnownEventIndicator -> TimelineItemInvisibleIndicatorModel + is VirtualTimelineItem.LastForwardIndicator -> TimelineItemLastForwardIndicatorModel } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemInvisibleIndicatorModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLastForwardIndicatorModel.kt similarity index 81% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemInvisibleIndicatorModel.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLastForwardIndicatorModel.kt index 2f4a118746..c532538722 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemInvisibleIndicatorModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLastForwardIndicatorModel.kt @@ -16,6 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual -data object TimelineItemInvisibleIndicatorModel : TimelineItemVirtualModel { - override val type: String = "TimelineItemInvisibleIndicatorModel" +data object TimelineItemLastForwardIndicatorModel: TimelineItemVirtualModel { + override val type: String = "TimelineItemLastForwardIndicatorModel" } 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 2879225553..5963f2c2d0 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 @@ -29,7 +29,7 @@ sealed interface VirtualTimelineItem { data object RoomBeginning: VirtualTimelineItem - data object LatestKnownEventIndicator: VirtualTimelineItem + data object LastForwardIndicator: VirtualTimelineItem data class LoadingIndicator( val direction: Timeline.PaginationDirection, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 1e572d925d..71c331021b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -45,12 +45,11 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage 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.InvisibleIndicatorPostProcessor +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.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -116,7 +115,7 @@ class RustTimeline( private val roomBeginningPostProcessor = RoomBeginningPostProcessor() private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock) - private val invisibleIndicatorPostProcessor = InvisibleIndicatorPostProcessor(isLive) + private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(isLive) private val timelineItemFactory = MatrixTimelineItemMapper( fetchDetailsForEvent = this::fetchDetailsForEvent, @@ -225,7 +224,8 @@ class RustTimeline( hasMoreToLoadBackwards = hasMoreToLoadBackward ) }.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) } - .let { items -> invisibleIndicatorPostProcessor.process(items) } + // Keep lastForwardIndicatorsPostProcessor last + .let { items -> lastForwardIndicatorsPostProcessor.process(items) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/InvisibleIndicatorPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/InvisibleIndicatorPostProcessor.kt deleted file mode 100644 index 7c7f819ed7..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/InvisibleIndicatorPostProcessor.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 - -class InvisibleIndicatorPostProcessor( - private val isLive: Boolean, -) { - private val latestEventIdentifiers: MutableSet = HashSet() - - fun process( - items: List, - ): List { - if (isLive) { - return items - } else { - return buildList { - items.forEach { item -> - add(item) - if (item is MatrixTimelineItem.Event) { - if (latestEventIdentifiers.contains(item.uniqueId)) { - add(createLatestKnownEventIndicator(item.uniqueId)) - } - } - } - items.latestEventIdentifier()?.let { latestEventIdentifier -> - if (latestEventIdentifiers.add(latestEventIdentifier)) { - add(createLatestKnownEventIndicator(latestEventIdentifier)) - } - } - } - } - } - - private fun createLatestKnownEventIndicator(identifier: String): MatrixTimelineItem { - return MatrixTimelineItem.Virtual( - uniqueId = "latest_known_event_$identifier", - virtual = VirtualTimelineItem.LatestKnownEventIndicator - ) - } - - private fun List.latestEventIdentifier(): String? { - return findLast { - when (it) { - is MatrixTimelineItem.Event -> true - else -> false - } - }?.let { - (it as MatrixTimelineItem.Event).uniqueId - } - } - - private fun List.indexOf(identifier: String): Int { - return indexOfLast { - when (it) { - is MatrixTimelineItem.Event -> { - it.uniqueId == identifier - } - else -> false - } - } - } -} 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..ea7337bd9e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt @@ -0,0 +1,76 @@ +/* + * 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 { + when (it) { + is MatrixTimelineItem.Event -> true + else -> false + } + }?.let { + (it as MatrixTimelineItem.Event).uniqueId + } ?: "fake_id" +} From d20605b8befa4a0c5525d5131134b9a1790efa94 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 12:55:08 +0200 Subject: [PATCH 19/53] Fix compilation issue (develop has been merged here) --- .../android/libraries/matrix/impl/timeline/RustTimeline.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 71c331021b..6b73bb3272 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -181,7 +181,7 @@ class RustTimeline( updatePaginationStatus(direction) { it.copy(isPaginating = true) } when (direction) { Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(PAGINATION_SIZE.toUShort()) - Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(PAGINATION_SIZE.toUShort()) + Timeline.PaginationDirection.FORWARDS -> inner.focusedPaginateForwards(PAGINATION_SIZE.toUShort()) } }.onFailure { error -> updatePaginationStatus(direction) { it.copy(isPaginating = false) } From 21ddeb00fe0cfff247e2d02eb0ff3a9f554dc87f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 13:57:08 +0200 Subject: [PATCH 20/53] Format and cleanup files --- .../messages/impl/MessagesFlowNode.kt | 2 +- .../impl/forward/ForwardMessagesPresenter.kt | 1 - .../impl/timeline/TimelineController.kt | 1 - .../messages/impl/timeline/TimelineEvents.kt | 2 +- .../impl/timeline/TimelineItemIndexer.kt | 1 - .../messages/impl/timeline/TimelineState.kt | 8 +- .../impl/timeline/TimelineStateProvider.kt | 1 - .../messages/impl/timeline/TimelineView.kt | 4 +- .../timeline/components/TimelineItemRow.kt | 1 - .../virtual/TimelineLoadingMoreIndicator.kt | 1 - .../factories/TimelineItemsFactory.kt | 1 - .../TimelineItemLastForwardIndicatorModel.kt | 2 +- .../MessageComposerPresenterTest.kt | 7 +- .../impl/timeline/TimelinePresenterTest.kt | 2 - .../poll/impl/history/PollHistoryPresenter.kt | 2 - .../impl/history/PollHistoryPresenterTest.kt | 9 +- .../designsystem/theme/ColorAliases.kt | 1 - .../libraries/matrix/api/room/MatrixRoom.kt | 3 +- .../libraries/matrix/api/timeline/Timeline.kt | 4 - .../matrix/api/timeline/TimelineProvider.kt | 3 - .../item/virtual/VirtualTimelineItem.kt | 6 +- .../matrix/impl/room/RustMatrixRoom.kt | 26 +- .../matrix/impl/timeline/RustTimeline.kt | 14 +- .../LastForwardIndicatorsPostProcessor.kt | 1 - .../LoadingIndicatorsPostProcessor.kt | 1 - .../RoomBeginningPostProcessor.kt | 3 - .../matrix/test/timeline/FakeTimeline.kt | 239 +++++++++++++++--- 27 files changed, 235 insertions(+), 111 deletions(-) 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 c77907a855..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 @@ -99,7 +99,7 @@ class MessagesFlowNode @AssistedInject constructor( data object Empty : NavTarget @Parcelize - data object Messages: NavTarget + data object Messages : NavTarget @Parcelize data class MediaViewer( 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 a2212b69f3..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,6 @@ 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 diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index c4d794b7c4..c7bd1b9b63 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -53,7 +53,6 @@ import kotlin.coroutines.cancellation.CancellationException class TimelineController @Inject constructor( private val room: MatrixRoom, ) : Closeable, TimelineProvider { - private val coroutineScope = CoroutineScope(SupervisorJob()) private val liveTimeline = MutableStateFlow(room.liveTimeline) 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 2edd14b160..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 @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline sealed interface TimelineEvents { data class OnScrollFinished(val firstIndex: Int) : TimelineEvents data class FocusOnEvent(val eventId: EventId) : TimelineEvents - data object ClearFocusRequestState: TimelineEvents + data object ClearFocusRequestState : TimelineEvents data object JumpToLive : TimelineEvents /** 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 index 4507dea99d..d11de93079 100644 --- 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 @@ -25,7 +25,6 @@ import javax.inject.Inject @SingleIn(RoomScope::class) class TimelineItemIndexer @Inject constructor() { - private val timelineEventsIndexes = mutableMapOf() fun isKnown(eventId: EventId): Boolean { 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 67a7dac574..592af0693d 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 @@ -29,18 +29,18 @@ data class TimelineState( val renderReadReceipts: Boolean, val newEventState: NewEventState, val isLive: Boolean, - val focusedEventId : EventId?, + val focusedEventId: EventId?, val focusRequestState: FocusRequestState, val eventSink: (TimelineEvents) -> Unit, -){ +) { val hasAnyEvent = timelineItems.any { it is TimelineItem.Event } } sealed interface FocusRequestState { data object None : FocusRequestState - data class Cached(val index: Int): FocusRequestState + data class Cached(val index: Int) : FocusRequestState data object Fetching : FocusRequestState - data object Fetched: FocusRequestState + data object Fetched : FocusRequestState data class Failure(val throwable: Throwable) : FocusRequestState } 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 3002c51d78..b5dff2b54e 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 @@ -71,7 +71,6 @@ fun aPaginationStatus( ) } - internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList { return persistentListOf( // 3 items (First Middle Last) with isMine = false 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 101ad58703..de8d198fd3 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 @@ -93,7 +93,6 @@ fun TimelineView( modifier: Modifier = Modifier, forceJumpToBottomVisibility: Boolean = false ) { - fun clearFocusRequestState() { state.eventSink(TimelineEvents.ClearFocusRequestState) } @@ -124,8 +123,7 @@ fun TimelineView( reverseLayout = useReverseLayout, contentPadding = PaddingValues(vertical = 8.dp), ) { - - if(state.isLive) { + if (state.isLive) { item { TypingNotificationView(state = typingNotificationState) } 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 4782ff21f1..b54e0a51ac 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 @@ -57,7 +57,6 @@ internal fun TimelineItemRow( eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier ) { - val backgroundModifier = if (timelineItem.isEvent(focusedEventId)) { Modifier.focusedEvent() } else { 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 90662957be..0af4f600ee 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 @@ -56,7 +56,6 @@ internal fun TimelineLoadingMoreIndicator( ) } } - } } 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 ac9803506c..8b5b644a23 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 @@ -34,7 +34,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext 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 index c532538722..b0b6376f50 100644 --- 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 @@ -16,6 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual -data object TimelineItemLastForwardIndicatorModel: TimelineItemVirtualModel { +data object TimelineItemLastForwardIndicatorModel : TimelineItemVirtualModel { override val type: String = "TimelineItemLastForwardIndicatorModel" } 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 c30d427239..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 @@ -365,7 +365,7 @@ class MessageComposerPresenterTest { @Test fun `present - reply message`() = runTest { - val replyMessageLambda = lambdaRecorder {_: EventId, _: String, _: String?, _:List -> + val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List -> Result.success(Unit) } val timeline = FakeTimeline().apply { @@ -396,7 +396,7 @@ class MessageComposerPresenterTest { assert(replyMessageLambda) .isCalledOnce() - .with(any(),value(A_REPLY),value(A_REPLY),any()) + .with(any(), value(A_REPLY), value(A_REPLY), any()) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -873,7 +873,7 @@ class MessageComposerPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - send messages with intentional mentions`() = runTest { - val replyMessageLambda = lambdaRecorder {_: EventId, _: String, _: String?, _:List -> + val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List -> Result.success(Unit) } val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List -> @@ -940,7 +940,6 @@ class MessageComposerPresenterTest { .isCalledOnce() .with(any(), any(), any(), any(), value(listOf(Mention.User(A_USER_ID_3)))) - skipItems(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 3dd00021fe..2161b11a5b 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 @@ -33,7 +33,6 @@ 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 @@ -69,7 +68,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest 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 7a3b3f8232..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,11 +32,9 @@ 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.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject 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 07f71f51d3..f07a2ce16a 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 @@ -61,9 +61,10 @@ class PollHistoryPresenterTest { private val timeline = FakeTimeline( timelineItems = aPollTimelineItems( mapOf( - AN_EVENT_ID to anOngoingPollContent(), - AN_EVENT_ID_2 to anEndedPollContent() - )), + AN_EVENT_ID to anOngoingPollContent(), + AN_EVENT_ID_2 to anEndedPollContent() + ) + ), backwardPaginationStatus = backwardPaginationStatus ) private val room = FakeMatrixRoom( @@ -136,7 +137,7 @@ class PollHistoryPresenterTest { @Test fun `present - load more scenario`() = runTest { - val paginateLambda = lambdaRecorder{ _: Timeline.PaginationDirection -> + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> Result.success(false) } timeline.apply { 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 1fedcceedd..1cd67e15f7 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 @@ -153,7 +153,6 @@ val SemanticColors.bigCheckmarkBorderColor val SemanticColors.highlightedMessageBackgroundColor get() = if (isLight) LightColorTokens.colorGreen300 else DarkColorTokens.colorGreen300 - @PreviewsDayNight @Composable internal fun ColorAliasesPreview() = ElementPreview { 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 1e9b926aaf..ad27a0eb36 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,9 +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.TimelineProvider -import io.element.android.libraries.matrix.api.timeline.Timeline 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 diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index c15c4b323f..6d0850db83 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.StateFlow import java.io.File interface Timeline : AutoCloseable { - data class PaginationStatus( val isPaginating: Boolean, val hasMoreToLoad: Boolean, @@ -52,7 +51,6 @@ interface Timeline : AutoCloseable { 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 @@ -164,6 +162,4 @@ interface Timeline : AutoCloseable { 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 index 5b55b8dc47..f8e0965c7a 100644 --- 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 @@ -19,11 +19,9 @@ package io.element.android.libraries.matrix.api.timeline import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.MatrixRoom -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf import javax.inject.Inject /** @@ -45,4 +43,3 @@ class LiveTimelineProvider @Inject constructor( ) : TimelineProvider { override fun activeTimelineFlow(): StateFlow = MutableStateFlow(room.liveTimeline) } - 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 5963f2c2d0..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 @@ -27,12 +27,12 @@ sealed interface VirtualTimelineItem { data object EncryptedHistoryBanner : VirtualTimelineItem - data object RoomBeginning: VirtualTimelineItem + data object RoomBeginning : VirtualTimelineItem - data object LastForwardIndicator: VirtualTimelineItem + data object LastForwardIndicator : VirtualTimelineItem data class LoadingIndicator( val direction: Timeline.PaginationDirection, val timestamp: Long, - ): VirtualTimelineItem + ) : VirtualTimelineItem } 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 c99c9a8c21..0043d52545 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 @@ -47,7 +47,6 @@ 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.media.MediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper @@ -78,14 +77,10 @@ import kotlinx.coroutines.withContext 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 uniffi.matrix_sdk.RoomPowerLevelChanges import java.io.File @@ -151,7 +146,7 @@ class RustMatrixRoom( private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow - override val liveTimeline = createTimeline(innerTimeline, isLive = true){ + override val liveTimeline = createTimeline(innerTimeline, isLive = true) { _syncUpdateFlow.value = systemClock.epochMillis() } @@ -179,7 +174,7 @@ class RustMatrixRoom( eventId = eventId.value, numContextEvents = 50u, internalIdPrefix = "focus_$eventId", - ).let {inner -> + ).let { inner -> createTimeline(inner, isLive = false) } } @@ -322,7 +317,7 @@ class RustMatrixRoom( } } - override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result { + override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result { return liveTimeline.sendMessage(body, htmlBody, mentions) } @@ -340,7 +335,7 @@ class RustMatrixRoom( return liveTimeline.enterSpecialMode(eventId) } - override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result{ + override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result { return liveTimeline.replyMessage(eventId, body, htmlBody, mentions) } @@ -446,11 +441,11 @@ class RustMatrixRoom( return liveTimeline.sendFile(file, fileInfo, progressCallback) } - override suspend fun toggleReaction(emoji: String, eventId: EventId): Result{ + override suspend fun toggleReaction(emoji: String, eventId: EventId): Result { return liveTimeline.toggleReaction(emoji, eventId) } - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result{ + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result { return liveTimeline.forwardEvent(eventId, roomIds) } @@ -458,7 +453,7 @@ class RustMatrixRoom( return liveTimeline.retrySendMessage(transactionId) } - override suspend fun cancelSend(transactionId: TransactionId): Result{ + override suspend fun cancelSend(transactionId: TransactionId): Result { return liveTimeline.cancelSend(transactionId) } @@ -563,14 +558,14 @@ class RustMatrixRoom( override suspend fun sendPollResponse( pollStartId: EventId, answers: List - ): Result { + ): Result { return liveTimeline.sendPollResponse(pollStartId, answers) } override suspend fun endPoll( pollStartId: EventId, text: String - ): Result { + ): Result { return liveTimeline.endPoll(pollStartId, text) } @@ -579,7 +574,7 @@ class RustMatrixRoom( audioInfo: AudioInfo, waveform: List, progressCallback: ProgressCallback?, - ): Result{ + ): Result { return liveTimeline.sendVoiceMessage(file, audioInfo, waveform, progressCallback) } @@ -634,5 +629,4 @@ class RustMatrixRoom( inner = timeline, ) } - } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 6b73bb3272..4f67f00e57 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -89,17 +89,16 @@ private const val PAGINATION_SIZE = 50 class RustTimeline( private val inner: InnerTimeline, - private val isLive: Boolean, - private val systemClock: SystemClock, - private val roomCoroutineScope: CoroutineScope, - private val isKeyBackupEnabled: Boolean, + isLive: Boolean, + systemClock: SystemClock, + roomCoroutineScope: CoroutineScope, + isKeyBackupEnabled: Boolean, private val matrixRoom: MatrixRoom, private val dispatcher: CoroutineDispatcher, - private val lastLoginTimestamp: Date?, + lastLoginTimestamp: Date?, private val roomContentForwarder: RoomContentForwarder, private val onNewSyncedEvent: () -> Unit, ) : Timeline { - private val initLatch = CompletableDeferred() private val isInit = AtomicBoolean(false) @@ -173,7 +172,7 @@ class RustTimeline( } } - // Use NonCancellable to avoid breaking the timeline when the coroutine is cancelled. + // 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 { @@ -226,7 +225,6 @@ class RustTimeline( }.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) } // Keep lastForwardIndicatorsPostProcessor last .let { items -> lastForwardIndicatorsPostProcessor.process(items) } - } override fun close() { 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 index ea7337bd9e..8db3952f0d 100644 --- 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 @@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime class LastForwardIndicatorsPostProcessor( private val isTimelineLive: Boolean, ) { - private val lastForwardIdentifiers = LinkedHashSet() fun process( 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 index c2db96730b..7e24f89c1c 100644 --- 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 @@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime import io.element.android.services.toolbox.api.systemclock.SystemClock class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) { - fun process( items: List, hasMoreToLoadBackward: Boolean, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index da8392a452..e3fa9c8072 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -29,7 +29,6 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime * or add the RoomBeginning item for non DM room. */ class RoomBeginningPostProcessor { - fun process( items: List, isDm: Boolean, @@ -49,7 +48,6 @@ class RoomBeginningPostProcessor { } 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 @@ -83,5 +81,4 @@ class RoomBeginningPostProcessor { virtual = VirtualTimelineItem.RoomBeginning ) } - } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index 5663ad6b5b..c99698fcb4 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -32,7 +32,6 @@ 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 io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -55,14 +54,37 @@ class FakeTimeline( ), override val membershipChangeEventReceived: Flow = MutableSharedFlow(), ) : Timeline { + var sendMessageLambda: ( + body: String, + htmlBody: String?, + mentions: List, + ) -> Result = { _, _, _ -> + Result.success(Unit) + } - 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) + 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) } + 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( + override suspend fun editMessage( + originalEventId: EventId?, + transactionId: TransactionId?, + body: String, + htmlBody: String?, + mentions: List, + ): Result = editMessageLambda( originalEventId, transactionId, body, @@ -70,23 +92,52 @@ class FakeTimeline( mentions ) - var enterSpecialModeLambda: (eventId: EventId?) -> Result = { Result.success(Unit) } + 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) } + 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( + 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()) } + 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( + override suspend fun sendImage( + file: File, + thumbnailFile: File?, + imageInfo: ImageInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ): Result = sendImageLambda( file, thumbnailFile, imageInfo, @@ -95,10 +146,25 @@ class FakeTimeline( progressCallback ) - var sendVideoLambda: (file: File, thumbnailFile: File?, videoInfo: VideoInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?) -> Result = - { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } + 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( + override suspend fun sendVideo( + file: File, + thumbnailFile: File?, + videoInfo: VideoInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ): Result = sendVideoLambda( file, thumbnailFile, videoInfo, @@ -107,19 +173,37 @@ class FakeTimeline( progressCallback ) - var sendAudioLambda: (file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?) -> Result = - { _, _, _ -> Result.success(FakeMediaUploadHandler()) } + var sendAudioLambda: ( + file: File, + audioInfo: AudioInfo, + progressCallback: ProgressCallback?, + ) -> Result = { _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } - override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result = sendAudioLambda( + 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()) } + var sendFileLambda: ( + file: File, + fileInfo: FileInfo, + progressCallback: ProgressCallback?, + ) -> Result = { _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } - override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result = sendFileLambda( + override suspend fun sendFile( + file: File, + fileInfo: FileInfo, + progressCallback: ProgressCallback?, + ): Result = sendFileLambda( file, fileInfo, progressCallback @@ -137,10 +221,23 @@ class FakeTimeline( 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) } + 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( + override suspend fun sendLocation( + body: String, + geoUri: String, + description: String?, + zoomLevel: Int?, + assetType: AssetType?, + ): Result = sendLocationLambda( body, geoUri, description, @@ -148,20 +245,44 @@ class FakeTimeline( assetType ) - var createPollLambda: (question: String, answers: List, maxSelections: Int, pollKind: PollKind) -> Result = - { _, _, _, _ -> Result.success(Unit) } + 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( + 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) } + 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( + override suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result = editPollLambda( pollStartId, question, answers, @@ -169,29 +290,67 @@ class FakeTimeline( pollKind ) - var sendPollResponseLambda: (pollStartId: EventId, answers: List) -> Result = { _, _ -> Result.success(Unit) } - override suspend fun sendPollResponse(pollStartId: EventId, answers: List): Result = sendPollResponseLambda(pollStartId, answers) + var sendPollResponseLambda: ( + pollStartId: EventId, + answers: List, + ) -> Result = { _, _ -> + Result.success(Unit) + } - var endPollLambda: (pollStartId: EventId, text: String) -> Result = { _, _ -> Result.success(Unit) } - override suspend fun endPoll(pollStartId: EventId, text: String): Result = endPollLambda(pollStartId, text) + override suspend fun sendPollResponse( + pollStartId: EventId, + answers: List, + ): Result = sendPollResponseLambda(pollStartId, answers) - var sendVoiceMessageLambda: (file: File, audioInfo: AudioInfo, waveform: List, progressCallback: ProgressCallback?) -> Result = - { _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } + var endPollLambda: ( + pollStartId: EventId, + text: String, + ) -> Result = { _, _ -> + Result.success(Unit) + } - override suspend fun sendVoiceMessage(file: File, audioInfo: AudioInfo, waveform: List, progressCallback: ProgressCallback?): Result = sendVoiceMessageLambda( + 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) } + 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) } + 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 { From 31946a98ad79c99b1c1cb13867e3383dec392255 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 14:12:17 +0200 Subject: [PATCH 21/53] Add preview for focusedEvent. --- .../features/messages/impl/MessagesStateProvider.kt | 2 ++ .../messages/impl/timeline/TimelineStateProvider.kt | 3 ++- .../features/messages/impl/timeline/TimelineView.kt | 13 ++++++++----- .../components/TimelineItemGroupedEventsRow.kt | 5 +++-- 4 files changed, 15 insertions(+), 8 deletions(-) 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/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index b5dff2b54e..12820660ab 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 @@ -49,6 +49,7 @@ fun aTimelineState( timelineItems: ImmutableList = persistentListOf(), renderReadReceipts: Boolean = false, timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), + focusedEventIndex: Int = -1, eventSink: (TimelineEvents) -> Unit = {}, ) = TimelineState( timelineItems = timelineItems, @@ -56,7 +57,7 @@ fun aTimelineState( renderReadReceipts = renderReadReceipts, newEventState = NewEventState.None, isLive = true, - focusedEventId = null, + focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId, focusRequestState = FocusRequestState.None, eventSink = eventSink, ) 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 de8d198fd3..119e358476 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 @@ -265,8 +265,8 @@ private fun BoxScope.TimelineScrollHelper( // Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive, modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 24.dp, bottom = 12.dp), + .align(Alignment.BottomEnd) + .padding(end = 24.dp, bottom = 12.dp), onClick = { jumpToBottom() }, ) } @@ -293,8 +293,8 @@ private fun JumpToBottomButton( ) { Icon( modifier = Modifier - .size(24.dp) - .rotate(90f), + .size(24.dp) + .rotate(90f), imageVector = CompoundIcons.ArrowRight(), contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom) ) @@ -312,7 +312,10 @@ internal fun TimelineViewPreview( LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(), ) { TimelineView( - state = aTimelineState(timelineItems), + state = aTimelineState( + timelineItems = timelineItems, + focusedEventIndex = 0, + ), typingNotificationState = aTypingNotificationState(), onUserDataClicked = {}, onLinkClicked = {}, 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 35571278bb..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 @@ -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(), - focusedEventId = null, + focusedEventId = events.events.first().eventId, renderReadReceipts = true, isLastOutgoingMessage = false, onClick = {}, From 174b1a4888970f475f26e59c0ec1d3a6c4936dea Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 14:28:09 +0200 Subject: [PATCH 22/53] Improve focusedEvent rendering when sender information are displayed. --- .../timeline/components/TimelineItemRow.kt | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) 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 b54e0a51ac..dafb244f66 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 @@ -24,6 +24,7 @@ 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 @@ -58,7 +59,12 @@ internal fun TimelineItemRow( modifier: Modifier = Modifier ) { val backgroundModifier = if (timelineItem.isEvent(focusedEventId)) { - Modifier.focusedEvent() + val focusedEventOffset = if ((timelineItem as? TimelineItem.Event)?.showSenderInformation == true) { + 14.dp + } else { + 2.dp + } + Modifier.focusedEvent(focusedEventOffset) } else { Modifier } @@ -130,18 +136,28 @@ internal fun TimelineItemRow( } @Composable -private fun Modifier.focusedEvent(): Modifier { +private fun Modifier.focusedEvent( + focusedEventOffset: Dp +): Modifier { val highlightedLineColor = ElementTheme.colors.textActionAccent val gradientColors = listOf( ElementTheme.colors.highlightedMessageBackgroundColor, ElementTheme.materialColors.background ) - val verticalOffset = 2.dp.toPx() + val verticalOffset = focusedEventOffset.toPx() return drawWithCache { val brush = Brush.verticalGradient(gradientColors) onDrawBehind { - drawRect(brush, topLeft = Offset(0f, verticalOffset), size = Size(size.width, size.height * 0.7f)) - drawLine(highlightedLineColor, start = Offset(0f, verticalOffset), end = Offset(size.width, verticalOffset)) + drawRect( + brush, + topLeft = Offset(0f, verticalOffset), + size = Size(size.width, size.height * 0.7f) + ) + drawLine( + highlightedLineColor, + start = Offset(0f, verticalOffset), + end = Offset(size.width, verticalOffset) + ) } }.padding(top = 4.dp) } From 1f0b7113551b4af2ba11886a172ad273ca17604b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 14:30:51 +0200 Subject: [PATCH 23/53] Add missing color preview. --- .../android/libraries/designsystem/theme/ColorAliases.kt | 2 ++ 1 file changed, 2 insertions(+) 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 1cd67e15f7..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 @@ -171,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, ) ) } From cd2b78459aaebf5ee34178318df8f4e96ea8f815 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 14:40:54 +0200 Subject: [PATCH 24/53] Fix issue with Gradient rendering. --- .../messages/impl/timeline/components/TimelineItemRow.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 dafb244f66..11ea953b2f 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 @@ -145,13 +145,17 @@ private fun Modifier.focusedEvent( ElementTheme.materialColors.background ) val verticalOffset = focusedEventOffset.toPx() + val verticalRatio = 0.7f return drawWithCache { - val brush = Brush.verticalGradient(gradientColors) + val brush = Brush.verticalGradient( + colors = gradientColors, + endY = size.height * verticalRatio, + ) onDrawBehind { drawRect( brush, topLeft = Offset(0f, verticalOffset), - size = Size(size.width, size.height * 0.7f) + size = Size(size.width, size.height * verticalRatio) ) drawLine( highlightedLineColor, From 471eb2cadc152ac588965bed28def67ee6226bcf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 14:47:01 +0200 Subject: [PATCH 25/53] Move LiveTimelineProvider to the test module. --- .../impl/timeline/TimelineController.kt | 4 +-- .../forward/ForwardMessagesPresenterTests.kt | 2 +- .../impl/create/CreatePollPresenterTest.kt | 2 +- .../impl/history/PollHistoryPresenterTest.kt | 2 +- .../matrix/api/timeline/TimelineProvider.kt | 15 ---------- .../test/timeline/LiveTimelineProvider.kt | 29 +++++++++++++++++++ 6 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/LiveTimelineProvider.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index c7bd1b9b63..2d3a4fbf7c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -21,7 +21,6 @@ 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.LiveTimelineProvider 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 @@ -46,10 +45,9 @@ import kotlin.coroutines.cancellation.CancellationException /** * 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). - * This controller will replace the [LiveTimelineProvider] in the DI. */ @SingleIn(RoomScope::class) -@ContributesBinding(RoomScope::class, boundType = TimelineProvider::class, replaces = [LiveTimelineProvider::class]) +@ContributesBinding(RoomScope::class, boundType = TimelineProvider::class) class TimelineController @Inject constructor( private val room: MatrixRoom, ) : Closeable, TimelineProvider { 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 f6c749ea32..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 @@ -22,11 +22,11 @@ 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.api.timeline.LiveTimelineProvider 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 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 0b33b7762f..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 @@ -31,12 +31,12 @@ 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.LiveTimelineProvider 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 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 f07a2ce16a..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 @@ -32,13 +32,13 @@ 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.LiveTimelineProvider 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 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 index f8e0965c7a..ebfaca48be 100644 --- 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 @@ -16,13 +16,8 @@ package io.element.android.libraries.matrix.api.timeline -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.room.MatrixRoom -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first -import javax.inject.Inject /** * This interface defines a way to get the active timeline. @@ -33,13 +28,3 @@ interface TimelineProvider { } suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().first() - -/** - * Default implementation of [TimelineProvider] that provides the live timeline of a room. - */ -@ContributesBinding(RoomScope::class) -class LiveTimelineProvider @Inject constructor( - private val room: MatrixRoom, -) : TimelineProvider { - override fun activeTimelineFlow(): StateFlow = MutableStateFlow(room.liveTimeline) -} 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) +} From ee7f0bac8b6340800bb1f7bd31081bad1d4c2c78 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 14:51:33 +0200 Subject: [PATCH 26/53] Improve preview of TimelineLoadingMoreIndicator --- .../components/virtual/TimelineLoadingMoreIndicator.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 0af4f600ee..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,6 +16,7 @@ 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 @@ -62,8 +63,11 @@ internal fun TimelineLoadingMoreIndicator( @PreviewsDayNight @Composable internal fun TimelineLoadingMoreIndicatorPreview() = ElementPreview { - Column { - TimelineLoadingMoreIndicator(Timeline.PaginationDirection.FORWARDS) + Column( + modifier = Modifier.padding(vertical = 2.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { TimelineLoadingMoreIndicator(Timeline.PaginationDirection.BACKWARDS) + TimelineLoadingMoreIndicator(Timeline.PaginationDirection.FORWARDS) } } From 9b4fa0f03d0234320f3fbcecf717fbbd833e2f9c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 15:23:24 +0200 Subject: [PATCH 27/53] Fix detekt issues --- .../android/features/messages/impl/timeline/TimelineView.kt | 3 ++- .../messages/impl/timeline/components/TimelineItemRow.kt | 1 + .../impl/timeline/components/TimelineItemVirtualRow.kt | 5 ++++- .../android/libraries/matrix/impl/timeline/RustTimeline.kt | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) 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 119e358476..039062dc87 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 @@ -235,6 +235,7 @@ private fun BoxScope.TimelineScrollHelper( } } + val latestOnClearFocusRequestState by rememberUpdatedState(onClearFocusRequestState) LaunchedEffect(focusRequestState) { if (focusRequestState is FocusRequestState.Cached) { if (abs(lazyListState.firstVisibleItemIndex - focusRequestState.index) < 10) { @@ -242,7 +243,7 @@ private fun BoxScope.TimelineScrollHelper( } else { lazyListState.scrollToItem(focusRequestState.index) } - onClearFocusRequestState() + latestOnClearFocusRequestState() } } 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 11ea953b2f..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 @@ -135,6 +135,7 @@ internal fun TimelineItemRow( } } +@Suppress("ModifierComposable") @Composable private fun Modifier.focusedEvent( focusedEventOffset: 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 635cda526e..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 @@ -20,6 +20,8 @@ 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 @@ -51,8 +53,9 @@ fun TimelineItemVirtualRow( TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name) is TimelineItemLoadingIndicatorModel -> { TimelineLoadingMoreIndicator(virtual.model.direction) + val latestEventSink by rememberUpdatedState(eventSink) LaunchedEffect(virtual.model.timestamp) { - eventSink(TimelineEvents.LoadMore(virtual.model.direction)) + latestEventSink(TimelineEvents.LoadMore(virtual.model.direction)) } } is TimelineItemLastForwardIndicatorModel -> { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 4f67f00e57..39469b552c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -222,7 +222,8 @@ class RustTimeline( isDm = matrixRoom.isDm, hasMoreToLoadBackwards = hasMoreToLoadBackward ) - }.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) } + } + .let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) } // Keep lastForwardIndicatorsPostProcessor last .let { items -> lastForwardIndicatorsPostProcessor.process(items) } } From a0f98bd20a3acb0486d7dc5f2a5c1951cd3d01f1 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 26 Apr 2024 13:37:58 +0000 Subject: [PATCH 28/53] Update screenshots --- ...ineLoadingMoreIndicator-Day-63_63_null,NEXUS_5,1.0,en].png | 4 ++-- ...eLoadingMoreIndicator-Night-63_64_null,NEXUS_5,1.0,en].png | 4 ++-- ...ventsRowContentExpanded-Day-25_25_null,NEXUS_5,1.0,en].png | 4 ++-- ...ntsRowContentExpanded-Night-25_26_null,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_TimelineView-Day-9_9_null_10,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_TimelineView-Day-9_9_null_11,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_TimelineView-Day-9_9_null_12,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_TimelineView-Day-9_9_null_13,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_TimelineView-Day-9_9_null_14,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_TimelineView-Day-9_9_null_15,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_TimelineView-Day-9_9_null_16,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...eView_null_TimelineView-Day-9_9_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...w_null_TimelineView-Night-9_10_null_10,NEXUS_5,1.0,en].png | 4 ++-- ...w_null_TimelineView-Night-9_10_null_11,NEXUS_5,1.0,en].png | 4 ++-- ...w_null_TimelineView-Night-9_10_null_12,NEXUS_5,1.0,en].png | 4 ++-- ...w_null_TimelineView-Night-9_10_null_13,NEXUS_5,1.0,en].png | 4 ++-- ...w_null_TimelineView-Night-9_10_null_14,NEXUS_5,1.0,en].png | 4 ++-- ...w_null_TimelineView-Night-9_10_null_15,NEXUS_5,1.0,en].png | 4 ++-- ...w_null_TimelineView-Night-9_10_null_16,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_TimelineView-Night-9_10_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...essagesViewWithTyping-Day-65_65_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...essagesViewWithTyping-Day-65_65_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...essagesViewWithTyping-Day-65_65_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...sagesViewWithTyping-Night-65_66_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...sagesViewWithTyping-Night-65_66_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...sagesViewWithTyping-Night-65_66_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png | 4 ++-- ...View_null_MessagesView-Day-0_0_null_12,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...sView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png | 4 ++-- ...ew_null_MessagesView-Night-0_1_null_12,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...orAliases_null_ColorAliases-Day_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...Aliases_null_ColorAliases-Night_1_null,NEXUS_5,1.0,en].png | 4 ++-- 70 files changed, 140 insertions(+), 140 deletions(-) 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 index 9b3c889d05..7bc7202be5 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c464ff6c892364dc096a8edba88074d891b9baa91f4ba50197c7a500d76c0fa7 -size 6247 +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 index e443bf7f5d..ea1240cd75 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03e53795df5b0078a7883a4ed181e2a022aeec9f095eaddeed213e3de9b16069 -size 6200 +oid sha256:492c2a10c3d643d24a2d578baf385a7019a86b39bf14f3d077b3233a8d7524a5 +size 6217 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 index d6cfb16784..0e898bf24c 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35bf09d08616a2b3867608ad948bbf3442b2c8a2560116ae36b01673cbbbf6f7 -size 14568 +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 index 94f097ddaf..4b10887481 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d662de6189258cb24f1c54fdb14ceb8e6a53620ce54643709999e5e7a9a184f -size 14603 +oid sha256:006894de328a6ec6a0d3ebac67145d695a4be790fbda9a39e932363a9f541d0e +size 23230 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 index c2df9c0ca7..f0789db8fe 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa33a2dc4094680f3aa5b5e98efec6c02d585d98ff94ba6351ba7589fa156438 -size 55386 +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-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 index a68d5922a0..c14bf5d943 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ba24739d9425c00794696d8a94b6261e605264da090a2c2702d5a81a67bfb46 -size 56180 +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-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 index a0cc74f6c6..e61c636936 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7bd915ebfc3de3703fc039a02064c03dce3013580fd25f47582b0b7612785b9 -size 52110 +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 index dcf2410f8e..111a343f3f 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22b316a3e0c0f9804578ac1d8e038c1087d2f2f3d71244a53f254a1d35d3512e -size 54131 +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-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 index 3b9d2f642b..42d3042545 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6b4053d9e86e963919d6462849cf0bfbaf9df7f14330e64046d9d3cb1f5fbdd -size 55030 +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-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 index a1eaf95736..5c71ee6478 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:daed63aa46f1d4e16de8b554616312b6a0d370768971894da346724ec9fdc145 -size 51012 +oid sha256:27167a8130d1ab01432d0f7a58d51328a1a0361943b5b0d7d258e42e09afafd9 +size 60802 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 From 03040e55077bf3d88bc6ea34d19961e70592c440 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 15:30:02 +0200 Subject: [PATCH 29/53] Do not ignore those tests. (was committed by mistake) --- .../location/impl/common/actions/AndroidLocationActionsTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt index 9665c886d7..6cd7cf82ce 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt @@ -18,11 +18,9 @@ package io.element.android.features.location.impl.common.actions import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location -import org.junit.Ignore import org.junit.Test import java.net.URLEncoder -@Ignore internal class AndroidLocationActionsTest { // We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII") From 7b55b0e327e9e42c344e0354de26eae4ea703cca Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 16:08:08 +0200 Subject: [PATCH 30/53] Add mapping on FocusEventException. Extract FocusRequestState to its own file and add preview. --- .../impl/timeline/TimelineController.kt | 26 +++---- .../messages/impl/timeline/TimelineView.kt | 24 +------ .../focus/FocusRequestStateProvider.kt | 45 +++++++++++++ .../timeline/focus/FocusRequestStateView.kt | 67 +++++++++++++++++++ .../libraries/matrix/api/room/MatrixRoom.kt | 2 +- .../api/room/errors/FocusEventException.kt | 34 ++++++++++ .../matrix/impl/room/FocusEventException.kt | 42 ++++++++++++ .../matrix/impl/room/RustMatrixRoom.kt | 21 +++--- .../matrix/test/room/FakeMatrixRoom.kt | 4 +- .../src/main/res/values/localazy.xml | 1 + 10 files changed, 219 insertions(+), 47 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/errors/FocusEventException.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/FocusEventException.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index 2d3a4fbf7c..bc12524045 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -24,6 +24,7 @@ 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 @@ -40,7 +41,6 @@ import kotlinx.coroutines.flow.stateIn import java.io.Closeable import java.util.Optional import javax.inject.Inject -import kotlin.coroutines.cancellation.CancellationException /** * This controller is responsible of using the right timeline to display messages and make associated actions. @@ -72,20 +72,20 @@ class TimelineController @Inject constructor( } suspend fun focusOnEvent(eventId: EventId): Result { - return try { - val newDetachedTimeline = room.timelineFocusedOnEvent(eventId) - detachedTimeline.getAndUpdate { current -> - if (current.isPresent) { - current.get().close() + 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) } - Optional.of(newDetachedTimeline) } - Result.success(Unit) - } catch (cancellation: CancellationException) { - throw cancellation - } catch (exception: Exception) { - Result.failure(exception) - } } /** 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 039062dc87..7f76dfe730 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 @@ -57,6 +57,7 @@ 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.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 @@ -64,8 +65,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.TypingNotificationView import io.element.android.features.messages.impl.typing.aTypingNotificationState -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.designsystem.theme.components.FloatingActionButton @@ -176,27 +175,6 @@ fun TimelineView( } } -@Composable -private fun FocusRequestStateView( - focusRequestState: FocusRequestState, - onClearFocusRequestState: () -> Unit, - modifier: Modifier = Modifier, -) { - when (focusRequestState) { - is FocusRequestState.Failure -> { - ErrorDialog( - content = stringResource(id = CommonStrings.common_failed), - onDismiss = onClearFocusRequestState, - modifier = modifier, - ) - } - FocusRequestState.Fetching -> { - ProgressDialog(modifier = modifier, onDismissRequest = onClearFocusRequestState) - } - else -> Unit - } -} - @Composable private fun BoxScope.TimelineScrollHelper( hasAnyEvent: Boolean, 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/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 ad27a0eb36..86e943fea9 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 @@ -100,7 +100,7 @@ interface MatrixRoom : Closeable { val liveTimeline: Timeline - suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline + suspend fun timelineFocusedOnEvent(eventId: EventId): Result fun destroy() 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/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 0043d52545..e0f992a367 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 @@ -169,13 +170,17 @@ class RustMatrixRoom( override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId) - override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline { - return innerRoom.timelineFocusedOnEvent( - eventId = eventId.value, - numContextEvents = 50u, - internalIdPrefix = "focus_$eventId", - ).let { inner -> - createTimeline(inner, isLive = false) + 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() } } @@ -442,7 +447,7 @@ class RustMatrixRoom( } override suspend fun toggleReaction(emoji: String, eventId: EventId): Result { - return liveTimeline.toggleReaction(emoji, eventId) + return liveTimeline.toggleReaction(emoji, eventId) } override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result { 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 17f9358df7..d3d89a0788 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 @@ -215,8 +215,8 @@ class FakeMatrixRoom( override val syncUpdateFlow: StateFlow = MutableStateFlow(0L) - override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline { - return FakeTimeline() + override suspend fun timelineFocusedOnEvent(eventId: EventId): Result { + return Result.success(FakeTimeline()) } override suspend fun subscribeToSync() = Unit diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 832a3df104..f25969332e 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -240,6 +240,7 @@ "Failed loading messages" "%1$s could not access your location. Please try again later." "Failed to upload your voice message." + "Message not found" "%1$s does not have permission to access your location. You can enable access in Settings." "%1$s does not have permission to access your location. Enable access below." "%1$s does not have permission to access your microphone. Enable access to record a voice message." From 5f1c1dcd2376c8a9bbef1bbbf96efd81269e5170 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 17:28:54 +0200 Subject: [PATCH 31/53] No need to have a MutableStateFlow for the live timeline. --- .../features/messages/impl/timeline/TimelineController.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index bc12524045..1352124454 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -35,6 +35,7 @@ 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 @@ -53,7 +54,7 @@ class TimelineController @Inject constructor( ) : Closeable, TimelineProvider { private val coroutineScope = CoroutineScope(SupervisorJob()) - private val liveTimeline = MutableStateFlow(room.liveTimeline) + private val liveTimeline = flowOf(room.liveTimeline) private val detachedTimeline = MutableStateFlow>(Optional.empty()) @OptIn(ExperimentalCoroutinesApi::class) From 16f0d9d4469b5f3e38eb865bfc40bf5b7857b978 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 17:29:28 +0200 Subject: [PATCH 32/53] Test TimelineController --- .../impl/timeline/TimelineControllerTest.kt | 217 ++++++++++++++++++ .../matrix/test/room/FakeMatrixRoom.kt | 10 +- .../matrix/test/timeline/FakeTimeline.kt | 10 +- 3 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt 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/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 d3d89a0788..7a6920e4dc 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 @@ -215,8 +215,14 @@ class FakeMatrixRoom( override val syncUpdateFlow: StateFlow = MutableStateFlow(0L) - override suspend fun timelineFocusedOnEvent(eventId: EventId): Result { - return Result.success(FakeTimeline()) + 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 diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index c99698fcb4..6ec8386651 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -39,6 +39,7 @@ 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( @@ -360,5 +361,12 @@ class FakeTimeline( } } - override fun close() = Unit + var closeCounter = 0 + private set + + override fun close() { + closeCounter++ + } + + override fun toString() = "FakeTimeline: $name" } From 890c899178f3268fd49cfd86a7644c842d3f9ba2 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 26 Apr 2024 16:37:12 +0000 Subject: [PATCH 33/53] Update screenshots --- ..._FocusRequestStateView-Day-65_65_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._FocusRequestStateView-Day-65_65_null_1,NEXUS_5,1.0,en].png | 3 +++ ..._FocusRequestStateView-Day-65_65_null_2,NEXUS_5,1.0,en].png | 3 +++ ..._FocusRequestStateView-Day-65_65_null_3,NEXUS_5,1.0,en].png | 3 +++ ...ocusRequestStateView-Night-65_66_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ocusRequestStateView-Night-65_66_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ocusRequestStateView-Night-65_66_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ocusRequestStateView-Night-65_66_null_3,NEXUS_5,1.0,en].png | 3 +++ ...essagesViewWithTyping-Day-66_66_null_0,NEXUS_5,1.0,en].png} | 0 ...essagesViewWithTyping-Day-66_66_null_1,NEXUS_5,1.0,en].png} | 0 ...essagesViewWithTyping-Day-66_66_null_2,NEXUS_5,1.0,en].png} | 0 ...sagesViewWithTyping-Night-66_67_null_0,NEXUS_5,1.0,en].png} | 0 ...sagesViewWithTyping-Night-66_67_null_1,NEXUS_5,1.0,en].png} | 0 ...sagesViewWithTyping-Night-66_67_null_2,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_0,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_1,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_2,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_3,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_4,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_5,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_6,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_7,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-67_67_null_8,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_0,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_1,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_2,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_3,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_4,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_5,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_6,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_7,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-67_68_null_8,NEXUS_5,1.0,en].png} | 0 32 files changed, 24 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_3,NEXUS_5,1.0,en].png rename 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 => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_6,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_7,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_8,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_6,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_7,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_8,NEXUS_5,1.0,en].png} (100%) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_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-65_65_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-65_65_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-65_65_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-65_65_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-65_65_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-65_65_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-65_65_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-65_65_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-65_65_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-65_65_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-65_65_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-65_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-Night-65_66_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-65_66_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-65_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-Night-65_66_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-65_66_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-65_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-Night-65_66_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-65_66_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-65_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-Night-65_66_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-65_66_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.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-66_66_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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_0,NEXUS_5,1.0,en].png 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-66_66_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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_1,NEXUS_5,1.0,en].png 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-66_66_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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-65_65_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_2,NEXUS_5,1.0,en].png 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-66_67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_null_0,NEXUS_5,1.0,en].png 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-66_67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_null_1,NEXUS_5,1.0,en].png 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-66_67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-65_66_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_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_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_67_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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_68_null_8,NEXUS_5,1.0,en].png From 2bc8d6e63274435070a17170fdb6c35779b97135 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 18:49:56 +0200 Subject: [PATCH 34/53] Add unit test on TimelineItemIndexer --- .../impl/timeline/TimelineItemIndexerTest.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexerTest.kt 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) + } +} From 5aff720c4e314d64dd4d66d082eccb0e8434c097 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 18:56:04 +0200 Subject: [PATCH 35/53] Make FocusRequestState immutable. --- .../android/features/messages/impl/timeline/TimelineState.kt | 1 + 1 file changed, 1 insertion(+) 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 592af0693d..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 @@ -36,6 +36,7 @@ data class TimelineState( val hasAnyEvent = timelineItems.any { it is TimelineItem.Event } } +@Immutable sealed interface FocusRequestState { data object None : FocusRequestState data class Cached(val index: Int) : FocusRequestState From de4b21cab1ecf4eeb74e10a3511b90bf20627c92 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 09:45:58 +0200 Subject: [PATCH 36/53] Remove unused method --- .../messages/impl/timeline/TimelineStateProvider.kt | 11 ----------- 1 file changed, 11 deletions(-) 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 12820660ab..f2670b7023 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.Timeline 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 @@ -62,16 +61,6 @@ fun aTimelineState( eventSink = eventSink, ) -fun aPaginationStatus( - isPaginating: Boolean = false, - hasMoreToLoad: Boolean = true, -): Timeline.PaginationStatus { - return Timeline.PaginationStatus( - isPaginating = isPaginating, - hasMoreToLoad = hasMoreToLoad, - ) -} - internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList { return persistentListOf( // 3 items (First Middle Last) with isMine = false From 0532f79a19ab41d3998e26f43ab1e08636199faf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 09:56:19 +0200 Subject: [PATCH 37/53] Extract extension `setTimelineView` --- .../impl/timeline/TimelineViewTest.kt | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) 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 0f99e8b558..d16de9f01d 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,11 +17,14 @@ 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.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.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams @@ -29,6 +32,7 @@ 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) @@ -38,53 +42,59 @@ 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( - timelineItems = persistentListOf( - TimelineItem.Virtual( - id = "backward_pagination", - model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0) - ), + rule.setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf( + TimelineItem.Virtual( + id = "backward_pagination", + model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0) ), - eventSink = eventsRecorder, ), - typingNotificationState = aTypingNotificationState(), - onUserDataClicked = EnsureNeverCalledWithParam(), - onLinkClicked = EnsureNeverCalledWithParam(), - onMessageClicked = EnsureNeverCalledWithParam(), - onMessageLongClicked = EnsureNeverCalledWithParam(), - onTimestampClicked = EnsureNeverCalledWithParam(), - onSwipeToReply = EnsureNeverCalledWithParam(), - onReactionClicked = EnsureNeverCalledWithTwoParams(), - onReactionLongClicked = EnsureNeverCalledWithTwoParams(), - onMoreReactionsClicked = EnsureNeverCalledWithParam(), - onReadReceiptClick = EnsureNeverCalledWithParam(), - ) - } + 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, - ), - typingNotificationState = aTypingNotificationState(), - 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, + ), + ) + } +} + +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(), +) { + 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, + ) } } From f0b90296077ec0378502170fac2e178015454766 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 10:07:20 +0200 Subject: [PATCH 38/53] Test TimelineView regarding "Jump to bottom" button. --- .../impl/timeline/TimelineStateProvider.kt | 3 +- .../impl/timeline/TimelineViewTest.kt | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) 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 f2670b7023..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 @@ -49,13 +49,14 @@ fun aTimelineState( renderReadReceipts: Boolean = false, timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), focusedEventIndex: Int = -1, + isLive: Boolean = true, eventSink: (TimelineEvents) -> Unit = {}, ) = TimelineState( timelineItems = timelineItems, timelineRoomInfo = timelineRoomInfo, renderReadReceipts = renderReadReceipts, newEventState = NewEventState.None, - isLive = true, + isLive = isLive, focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId, focusRequestState = FocusRequestState.None, eventSink = eventSink, 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 d16de9f01d..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 @@ -19,6 +19,8 @@ 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 @@ -26,6 +28,7 @@ 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 @@ -65,6 +68,34 @@ class TimelineViewTest { ), ) } + + @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( @@ -80,6 +111,7 @@ private fun AndroidComposeTestRule.setTimel onReactionLongClicked: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(), onMoreReactionsClicked: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + forceJumpToBottomVisibility: Boolean = false, ) { setContent { TimelineView( @@ -95,6 +127,7 @@ private fun AndroidComposeTestRule.setTimel onReactionLongClicked = onReactionLongClicked, onMoreReactionsClicked = onMoreReactionsClicked, onReadReceiptClick = onReadReceiptClick, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, ) } } From a5eb3f8f919456ecc29720690852864560d622f1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 12:15:14 +0200 Subject: [PATCH 39/53] `focusOnLive()` is not suspendable. --- .../features/messages/impl/timeline/TimelinePresenter.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 61b2add5cb..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 @@ -158,9 +158,7 @@ class TimelinePresenter @AssistedInject constructor( focusRequestState.value = FocusRequestState.None } is TimelineEvents.JumpToLive -> { - localScope.launch { - timelineController.focusOnLive() - } + timelineController.focusOnLive() } } } From b511f243555cf6d51ba35fe4026a5d795b5ac343 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 12:16:14 +0200 Subject: [PATCH 40/53] Add test on focusRequestState for event `TimelineEvents.FocusOnEvent` and `TimelineEvents.JumpToLive` --- .../impl/timeline/TimelinePresenterTest.kt | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) 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 2161b11a5b..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 @@ -464,6 +465,115 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" } } + @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 = FakeTimeline( @@ -523,6 +633,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" endPollAction: EndPollAction = FakeEndPollAction(), sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), + timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactory = timelineItemsFactory, @@ -534,7 +645,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" endPollAction = endPollAction, sendPollResponseAction = sendPollResponseAction, sessionPreferencesStore = sessionPreferencesStore, - timelineItemIndexer = TimelineItemIndexer(), + timelineItemIndexer = timelineItemIndexer, timelineController = TimelineController(room), ) } From 65db3d597a1e972777cc78bf93a0e50cc089a043 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 14:09:06 +0200 Subject: [PATCH 41/53] Remove unused `newTimelineById` --- .../messages/impl/timeline/factories/TimelineItemsFactory.kt | 2 -- 1 file changed, 2 deletions(-) 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 8b5b644a23..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 @@ -81,7 +81,6 @@ class TimelineItemsFactory @Inject constructor( roomMembers: List, ) { val newTimelineItemStates = ArrayList() - val newTimelineById = mutableMapOf() for (index in diffCache.indices().reversed()) { val cacheItem = diffCache.get(index) if (cacheItem == null) { @@ -98,7 +97,6 @@ class TimelineItemsFactory @Inject constructor( } else { cacheItem } - newTimelineById[updatedItem.identifier()] = updatedItem newTimelineItemStates.add(updatedItem) } } From c75995f925eded9731f5df4ba859ad8df45fac16 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 14:16:12 +0200 Subject: [PATCH 42/53] Add documentation on new API. --- .../android/libraries/matrix/api/room/MatrixRoom.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 86e943fea9..fc4ded247a 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 @@ -98,8 +98,15 @@ interface MatrixRoom : Closeable { val syncUpdateFlow: StateFlow + /** + * 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() From 48e9b780b17e0cee99f963515001602269ac27f8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 14:20:46 +0200 Subject: [PATCH 43/53] Code optimization. --- .../postprocessor/LastForwardIndicatorsPostProcessor.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 index 8db3952f0d..d717fac00d 100644 --- 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 @@ -65,10 +65,7 @@ private fun createLastForwardIndicator(identifier: String): MatrixTimelineItem { private fun List.latestEventIdentifier(): String { return findLast { - when (it) { - is MatrixTimelineItem.Event -> true - else -> false - } + it is MatrixTimelineItem.Event }?.let { (it as MatrixTimelineItem.Event).uniqueId } ?: "fake_id" From d42a96cc63eac74f4c13171c094ddc8a50eb27fa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 14:27:07 +0200 Subject: [PATCH 44/53] No need to keep `innerTimeline` as a class member. --- .../android/libraries/matrix/impl/room/RustMatrixRoom.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e0f992a367..43a097298c 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 @@ -94,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, From ee0a198c4c2e7240f2c407e6aecf8907d1eebfe6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 14:28:04 +0200 Subject: [PATCH 45/53] Remove unnecessary default value. --- .../android/libraries/matrix/impl/room/RustMatrixRoom.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 43a097298c..a981a64fd2 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 @@ -618,7 +618,7 @@ class RustMatrixRoom( private fun createTimeline( timeline: InnerTimeline, - isLive: Boolean = true, + isLive: Boolean, onNewSyncedEvent: () -> Unit = {}, ): Timeline { return RustTimeline( From 16bdd34a8052638c3a28bd70dc76ad651b3b2ea9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 15:48:58 +0200 Subject: [PATCH 46/53] Add log in case of error. --- .../android/libraries/matrix/impl/timeline/RustTimeline.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 39469b552c..198f231ccb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -299,6 +299,8 @@ class RustTimeline( 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?") } } From 6b8552fac1384bbbc11174f214e6824e19dca23f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 15:50:56 +0200 Subject: [PATCH 47/53] Ensure `editMessage`, `enterSpecialMode` and `replyMessage` are called on the current timeline. --- .../libraries/matrix/api/room/MatrixRoom.kt | 6 ----- .../matrix/impl/room/RustMatrixRoom.kt | 18 ------------- .../matrix/test/room/FakeMatrixRoom.kt | 26 ------------------- 3 files changed, 50 deletions(-) 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 fc4ded247a..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 @@ -131,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/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 a981a64fd2..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 @@ -326,24 +326,6 @@ class RustMatrixRoom( return liveTimeline.sendMessage(body, htmlBody, mentions) } - override suspend fun editMessage( - originalEventId: EventId?, - transactionId: TransactionId?, - body: String, - htmlBody: String?, - mentions: List, - ): Result { - return liveTimeline.editMessage(originalEventId, transactionId, body, htmlBody, mentions) - } - - override suspend fun enterSpecialMode(eventId: EventId?): Result { - return liveTimeline.enterSpecialMode(eventId) - } - - override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result { - return liveTimeline.replyMessage(eventId, body, htmlBody, mentions) - } - override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) { runCatching { innerRoom.redact(eventId.value, reason) 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 7a6920e4dc..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 @@ -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 @@ -296,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 From 2e2d1277e86a30ecc532ec03c6ed8fb0945ec464 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 17:41:25 +0200 Subject: [PATCH 48/53] Map all replyTo data and add preview for loading and erorr case. --- .../components/TimelineItemEventRow.kt | 78 +++++++++++++++---- ...melineItemEventRowWithReplyOtherPreview.kt | 40 ++++++++++ .../TimelineItemEventRowWithReplyPreview.kt | 2 +- .../impl/timeline/model/InReplyToDetails.kt | 25 +++--- .../impl/timeline/model/InReplyToMetadata.kt | 2 +- .../api/timeline/item/event/InReplyTo.kt | 7 +- .../timeline/item/event/EventMessageMapper.kt | 13 +++- 7 files changed, 138 insertions(+), 29 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt 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 b5c60bb2af..1b9655c471 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 @@ -94,6 +93,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI 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.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 @@ -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)) - // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent -// .clickable(enabled = true, onClick = inReplyToClick) - ) + 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) + 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/model/InReplyToDetails.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt index 83627b5b25..41fda8c294 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 @@ -27,18 +27,23 @@ 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?, -) +sealed class InReplyToDetails(val eventId: EventId) { + class Ready( + eventId: EventId, + val senderId: UserId, + val senderProfile: ProfileTimelineDetails, + val eventContent: EventContent?, + val textContent: String?, + ) : InReplyToDetails(eventId) + + class Loading(eventId: EventId) : InReplyToDetails(eventId) + class Error(eventId: EventId, val message: String) : InReplyToDetails(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 +60,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/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/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( From 03ccc8fe616a14dcb8797c826099d183202ec5a3 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 29 Apr 2024 15:50:42 +0000 Subject: [PATCH 49/53] Update screenshots --- ...mojiItem_null_EmojiItem-Day-34_34_null,NEXUS_5,1.0,en].png} | 0 ...jiItem_null_EmojiItem-Night-34_35_null,NEXUS_5,1.0,en].png} | 0 ...Picker_null_EmojiPicker-Day-35_35_null,NEXUS_5,1.0,en].png} | 0 ...cker_null_EmojiPicker-Night-35_36_null,NEXUS_5,1.0,en].png} | 0 ...ton_null_ProgressButton-Day-54_54_null,NEXUS_5,1.0,en].png} | 0 ...n_null_ProgressButton-Night-54_55_null,NEXUS_5,1.0,en].png} | 0 ...lineImageWithCaptionRow-Day-40_40_null,NEXUS_5,1.0,en].png} | 0 ...neImageWithCaptionRow-Night-40_41_null,NEXUS_5,1.0,en].png} | 0 ...TimelineItemAudioView-Day-36_36_null_0,NEXUS_5,1.0,en].png} | 0 ...TimelineItemAudioView-Day-36_36_null_1,NEXUS_5,1.0,en].png} | 0 ...TimelineItemAudioView-Day-36_36_null_2,NEXUS_5,1.0,en].png} | 0 ...melineItemAudioView-Night-36_37_null_0,NEXUS_5,1.0,en].png} | 0 ...melineItemAudioView-Night-36_37_null_1,NEXUS_5,1.0,en].png} | 0 ...melineItemAudioView-Night-36_37_null_2,NEXUS_5,1.0,en].png} | 0 ...melineItemEncryptedView-Day-37_37_null,NEXUS_5,1.0,en].png} | 0 ...lineItemEncryptedView-Night-37_38_null,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemFileView-Day-38_38_null_0,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemFileView-Day-38_38_null_1,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemFileView-Day-38_38_null_2,NEXUS_5,1.0,en].png} | 0 ...imelineItemFileView-Night-38_39_null_0,NEXUS_5,1.0,en].png} | 0 ...imelineItemFileView-Night-38_39_null_1,NEXUS_5,1.0,en].png} | 0 ...imelineItemFileView-Night-38_39_null_2,NEXUS_5,1.0,en].png} | 0 ...TimelineItemImageView-Day-39_39_null_0,NEXUS_5,1.0,en].png} | 0 ...TimelineItemImageView-Day-39_39_null_1,NEXUS_5,1.0,en].png} | 0 ...TimelineItemImageView-Day-39_39_null_2,NEXUS_5,1.0,en].png} | 0 ...melineItemImageView-Night-39_40_null_0,NEXUS_5,1.0,en].png} | 0 ...melineItemImageView-Night-39_40_null_1,NEXUS_5,1.0,en].png} | 0 ...melineItemImageView-Night-39_40_null_2,NEXUS_5,1.0,en].png} | 0 ...lineItemInformativeView-Day-41_41_null,NEXUS_5,1.0,en].png} | 0 ...neItemInformativeView-Night-41_42_null,NEXUS_5,1.0,en].png} | 0 ...temLegacyCallInviteView-Day-42_42_null,NEXUS_5,1.0,en].png} | 0 ...mLegacyCallInviteView-Night-42_43_null,NEXUS_5,1.0,en].png} | 0 ...elineItemLocationView-Day-43_43_null_0,NEXUS_5,1.0,en].png} | 0 ...elineItemLocationView-Day-43_43_null_1,NEXUS_5,1.0,en].png} | 0 ...ineItemLocationView-Night-43_44_null_0,NEXUS_5,1.0,en].png} | 0 ...ineItemLocationView-Night-43_44_null_1,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemPollView-Day-44_44_null_0,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemPollView-Day-44_44_null_1,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemPollView-Day-44_44_null_2,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemPollView-Day-44_44_null_3,NEXUS_5,1.0,en].png} | 0 ...imelineItemPollView-Night-44_45_null_0,NEXUS_5,1.0,en].png} | 0 ...imelineItemPollView-Night-44_45_null_1,NEXUS_5,1.0,en].png} | 0 ...imelineItemPollView-Night-44_45_null_2,NEXUS_5,1.0,en].png} | 0 ...imelineItemPollView-Night-44_45_null_3,NEXUS_5,1.0,en].png} | 0 ...imelineItemRedactedView-Day-45_45_null,NEXUS_5,1.0,en].png} | 0 ...elineItemRedactedView-Night-45_46_null,NEXUS_5,1.0,en].png} | 0 ...l_TimelineItemStateView-Day-46_46_null,NEXUS_5,1.0,en].png} | 0 ...TimelineItemStateView-Night-46_47_null,NEXUS_5,1.0,en].png} | 0 ...melineItemStickerView-Day-47_47_null_0,NEXUS_5,1.0,en].png} | 0 ...melineItemStickerView-Day-47_47_null_1,NEXUS_5,1.0,en].png} | 0 ...melineItemStickerView-Day-47_47_null_2,NEXUS_5,1.0,en].png} | 0 ...lineItemStickerView-Night-47_48_null_0,NEXUS_5,1.0,en].png} | 0 ...lineItemStickerView-Night-47_48_null_1,NEXUS_5,1.0,en].png} | 0 ...lineItemStickerView-Night-47_48_null_2,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemTextView-Day-48_48_null_0,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemTextView-Day-48_48_null_1,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemTextView-Day-48_48_null_2,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemTextView-Day-48_48_null_3,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemTextView-Day-48_48_null_4,NEXUS_5,1.0,en].png} | 0 ..._TimelineItemTextView-Day-48_48_null_5,NEXUS_5,1.0,en].png} | 0 ...imelineItemTextView-Night-48_49_null_0,NEXUS_5,1.0,en].png} | 0 ...imelineItemTextView-Night-48_49_null_1,NEXUS_5,1.0,en].png} | 0 ...imelineItemTextView-Night-48_49_null_2,NEXUS_5,1.0,en].png} | 0 ...imelineItemTextView-Night-48_49_null_3,NEXUS_5,1.0,en].png} | 0 ...imelineItemTextView-Night-48_49_null_4,NEXUS_5,1.0,en].png} | 0 ...imelineItemTextView-Night-48_49_null_5,NEXUS_5,1.0,en].png} | 0 ...TimelineItemUnknownView-Day-49_49_null,NEXUS_5,1.0,en].png} | 0 ...melineItemUnknownView-Night-49_50_null,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVideoView-Day-50_50_null_0,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVideoView-Day-50_50_null_1,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVideoView-Day-50_50_null_2,NEXUS_5,1.0,en].png} | 0 ...melineItemVideoView-Night-50_51_null_0,NEXUS_5,1.0,en].png} | 0 ...melineItemVideoView-Night-50_51_null_1,NEXUS_5,1.0,en].png} | 0 ...melineItemVideoView-Night-50_51_null_2,NEXUS_5,1.0,en].png} | 0 ...ineItemVoiceViewUnified-Day-53_53_null,NEXUS_5,1.0,en].png} | 0 ...eItemVoiceViewUnified-Night-53_54_null,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_0,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_1,NEXUS_5,1.0,en].png} | 0 ...imelineItemVoiceView-Day-52_52_null_10,NEXUS_5,1.0,en].png} | 0 ...imelineItemVoiceView-Day-52_52_null_11,NEXUS_5,1.0,en].png} | 0 ...imelineItemVoiceView-Day-52_52_null_12,NEXUS_5,1.0,en].png} | 0 ...imelineItemVoiceView-Day-52_52_null_13,NEXUS_5,1.0,en].png} | 0 ...imelineItemVoiceView-Day-52_52_null_14,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_2,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_3,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_4,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_5,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_6,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_7,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_8,NEXUS_5,1.0,en].png} | 0 ...TimelineItemVoiceView-Day-52_52_null_9,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_0,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_1,NEXUS_5,1.0,en].png} | 0 ...elineItemVoiceView-Night-52_53_null_10,NEXUS_5,1.0,en].png} | 0 ...elineItemVoiceView-Night-52_53_null_11,NEXUS_5,1.0,en].png} | 0 ...elineItemVoiceView-Night-52_53_null_12,NEXUS_5,1.0,en].png} | 0 ...elineItemVoiceView-Night-52_53_null_13,NEXUS_5,1.0,en].png} | 0 ...elineItemVoiceView-Night-52_53_null_14,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_2,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_3,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_4,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_5,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_6,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_7,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_8,NEXUS_5,1.0,en].png} | 0 ...melineItemVoiceView-Night-52_53_null_9,NEXUS_5,1.0,en].png} | 0 ...lineVideoWithCaptionRow-Day-51_51_null,NEXUS_5,1.0,en].png} | 0 ...neVideoWithCaptionRow-Night-51_52_null,NEXUS_5,1.0,en].png} | 0 ...ew_null_GroupHeaderView-Day-55_55_null,NEXUS_5,1.0,en].png} | 0 ..._null_GroupHeaderView-Night-55_56_null,NEXUS_5,1.0,en].png} | 0 ...ent_null_SheetContent-Day-56_56_null_0,NEXUS_5,1.0,en].png} | 0 ...t_null_SheetContent-Night-56_57_null_0,NEXUS_5,1.0,en].png} | 0 ...eadReceiptBottomSheet-Day-58_58_null_0,NEXUS_5,1.0,en].png} | 0 ...eadReceiptBottomSheet-Day-58_58_null_1,NEXUS_5,1.0,en].png} | 0 ...eadReceiptBottomSheet-Day-58_58_null_2,NEXUS_5,1.0,en].png} | 0 ...eadReceiptBottomSheet-Day-58_58_null_3,NEXUS_5,1.0,en].png} | 0 ...eadReceiptBottomSheet-Day-58_58_null_4,NEXUS_5,1.0,en].png} | 0 ...eadReceiptBottomSheet-Day-58_58_null_5,NEXUS_5,1.0,en].png} | 0 ...dReceiptBottomSheet-Night-58_59_null_0,NEXUS_5,1.0,en].png} | 0 ...dReceiptBottomSheet-Night-58_59_null_1,NEXUS_5,1.0,en].png} | 0 ...dReceiptBottomSheet-Night-58_59_null_2,NEXUS_5,1.0,en].png} | 0 ...dReceiptBottomSheet-Night-58_59_null_3,NEXUS_5,1.0,en].png} | 0 ...dReceiptBottomSheet-Night-58_59_null_4,NEXUS_5,1.0,en].png} | 0 ...dReceiptBottomSheet-Night-58_59_null_5,NEXUS_5,1.0,en].png} | 0 ...neItemReadReceiptView-Day-57_57_null_0,NEXUS_5,1.0,en].png} | 0 ...neItemReadReceiptView-Day-57_57_null_1,NEXUS_5,1.0,en].png} | 0 ...neItemReadReceiptView-Day-57_57_null_2,NEXUS_5,1.0,en].png} | 0 ...neItemReadReceiptView-Day-57_57_null_3,NEXUS_5,1.0,en].png} | 0 ...neItemReadReceiptView-Day-57_57_null_4,NEXUS_5,1.0,en].png} | 0 ...neItemReadReceiptView-Day-57_57_null_5,NEXUS_5,1.0,en].png} | 0 ...neItemReadReceiptView-Day-57_57_null_6,NEXUS_5,1.0,en].png} | 0 ...neItemReadReceiptView-Day-57_57_null_7,NEXUS_5,1.0,en].png} | 0 ...ItemReadReceiptView-Night-57_58_null_0,NEXUS_5,1.0,en].png} | 0 ...ItemReadReceiptView-Night-57_58_null_1,NEXUS_5,1.0,en].png} | 0 ...ItemReadReceiptView-Night-57_58_null_2,NEXUS_5,1.0,en].png} | 0 ...ItemReadReceiptView-Night-57_58_null_3,NEXUS_5,1.0,en].png} | 0 ...ItemReadReceiptView-Night-57_58_null_4,NEXUS_5,1.0,en].png} | 0 ...ItemReadReceiptView-Night-57_58_null_5,NEXUS_5,1.0,en].png} | 0 ...ItemReadReceiptView-Night-57_58_null_6,NEXUS_5,1.0,en].png} | 0 ...ItemReadReceiptView-Night-57_58_null_7,NEXUS_5,1.0,en].png} | 0 ..._RetrySendMessageMenu-Day-59_59_null_0,NEXUS_5,1.0,en].png} | 0 ..._RetrySendMessageMenu-Day-59_59_null_1,NEXUS_5,1.0,en].png} | 0 ...etrySendMessageMenu-Night-59_60_null_0,NEXUS_5,1.0,en].png} | 0 ...etrySendMessageMenu-Night-59_60_null_1,NEXUS_5,1.0,en].png} | 0 ...ryptedHistoryBannerView-Day-60_60_null,NEXUS_5,1.0,en].png} | 0 ...ptedHistoryBannerView-Night-60_61_null,NEXUS_5,1.0,en].png} | 0 ...eItemDaySeparatorView-Day-61_61_null_0,NEXUS_5,1.0,en].png} | 0 ...eItemDaySeparatorView-Day-61_61_null_1,NEXUS_5,1.0,en].png} | 0 ...temDaySeparatorView-Night-61_62_null_0,NEXUS_5,1.0,en].png} | 0 ...temDaySeparatorView-Night-61_62_null_1,NEXUS_5,1.0,en].png} | 0 ...elineItemReadMarkerView-Day-62_62_null,NEXUS_5,1.0,en].png} | 0 ...ineItemReadMarkerView-Night-62_63_null,NEXUS_5,1.0,en].png} | 0 ...neItemRoomBeginningView-Day-63_63_null,NEXUS_5,1.0,en].png} | 0 ...ItemRoomBeginningView-Night-63_64_null,NEXUS_5,1.0,en].png} | 0 ...ineLoadingMoreIndicator-Day-64_64_null,NEXUS_5,1.0,en].png} | 0 ...eLoadingMoreIndicator-Night-64_65_null,NEXUS_5,1.0,en].png} | 0 ...EventRowWithReplyOther-Day-24_24_null_0,NEXUS_5,1.0,en].png | 3 +++ ...EventRowWithReplyOther-Day-24_24_null_1,NEXUS_5,1.0,en].png | 3 +++ ...entRowWithReplyOther-Night-24_25_null_0,NEXUS_5,1.0,en].png | 3 +++ ...entRowWithReplyOther-Night-24_25_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ItemEventRowWithReply-Day-25_25_null_0,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_1,NEXUS_5,1.0,en].png} | 0 ...temEventRowWithReply-Day-25_25_null_10,NEXUS_5,1.0,en].png} | 0 ...temEventRowWithReply-Day-25_25_null_11,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_2,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_3,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_4,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_5,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_6,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_7,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_8,NEXUS_5,1.0,en].png} | 0 ...ItemEventRowWithReply-Day-25_25_null_9,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_0,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_1,NEXUS_5,1.0,en].png} | 0 ...mEventRowWithReply-Night-25_26_null_10,NEXUS_5,1.0,en].png} | 0 ...mEventRowWithReply-Night-25_26_null_11,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_2,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_3,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_4,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_5,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_6,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_7,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_8,NEXUS_5,1.0,en].png} | 0 ...emEventRowWithReply-Night-25_26_null_9,NEXUS_5,1.0,en].png} | 0 ...ventsRowContentCollapse-Day-27_27_null,NEXUS_5,1.0,en].png} | 0 ...ntsRowContentCollapse-Night-27_28_null,NEXUS_5,1.0,en].png} | 0 ...ventsRowContentExpanded-Day-26_26_null,NEXUS_5,1.0,en].png} | 0 ...ntsRowContentExpanded-Night-26_27_null,NEXUS_5,1.0,en].png} | 0 ...lineItemReactionsLayout-Day-28_28_null,NEXUS_5,1.0,en].png} | 0 ...neItemReactionsLayout-Night-28_29_null,NEXUS_5,1.0,en].png} | 0 ...ineItemReactionsViewFew-Day-30_30_null,NEXUS_5,1.0,en].png} | 0 ...eItemReactionsViewFew-Night-30_31_null,NEXUS_5,1.0,en].png} | 0 ...emReactionsViewIncoming-Day-31_31_null,NEXUS_5,1.0,en].png} | 0 ...ReactionsViewIncoming-Night-31_32_null,NEXUS_5,1.0,en].png} | 0 ...emReactionsViewOutgoing-Day-32_32_null,NEXUS_5,1.0,en].png} | 0 ...ReactionsViewOutgoing-Night-32_33_null,NEXUS_5,1.0,en].png} | 0 ...melineItemReactionsView-Day-29_29_null,NEXUS_5,1.0,en].png} | 0 ...lineItemReactionsView-Night-29_30_null,NEXUS_5,1.0,en].png} | 0 ...melineItemStateEventRow-Day-33_33_null,NEXUS_5,1.0,en].png} | 0 ...lineItemStateEventRow-Night-33_34_null,NEXUS_5,1.0,en].png} | 0 ...null_EventDebugInfoView-Day-65_65_null,NEXUS_5,1.0,en].png} | 0 ...ll_EventDebugInfoView-Night-65_66_null,NEXUS_5,1.0,en].png} | 0 ...FocusRequestStateView-Day-66_66_null_0,NEXUS_5,1.0,en].png} | 0 ...FocusRequestStateView-Day-66_66_null_1,NEXUS_5,1.0,en].png} | 0 ...FocusRequestStateView-Day-66_66_null_2,NEXUS_5,1.0,en].png} | 0 ...FocusRequestStateView-Day-66_66_null_3,NEXUS_5,1.0,en].png} | 0 ...cusRequestStateView-Night-66_67_null_0,NEXUS_5,1.0,en].png} | 0 ...cusRequestStateView-Night-66_67_null_1,NEXUS_5,1.0,en].png} | 0 ...cusRequestStateView-Night-66_67_null_2,NEXUS_5,1.0,en].png} | 0 ...cusRequestStateView-Night-66_67_null_3,NEXUS_5,1.0,en].png} | 0 ...essagesViewWithTyping-Day-67_67_null_0,NEXUS_5,1.0,en].png} | 0 ...essagesViewWithTyping-Day-67_67_null_1,NEXUS_5,1.0,en].png} | 0 ...essagesViewWithTyping-Day-67_67_null_2,NEXUS_5,1.0,en].png} | 0 ...sagesViewWithTyping-Night-67_68_null_0,NEXUS_5,1.0,en].png} | 0 ...sagesViewWithTyping-Night-67_68_null_1,NEXUS_5,1.0,en].png} | 0 ...sagesViewWithTyping-Night-67_68_null_2,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_0,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_1,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_2,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_3,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_4,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_5,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_6,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_7,NEXUS_5,1.0,en].png} | 0 ...ypingNotificationView-Day-68_68_null_8,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_0,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_1,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_2,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_3,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_4,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_5,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_6,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_7,NEXUS_5,1.0,en].png} | 0 ...ingNotificationView-Night-68_69_null_8,NEXUS_5,1.0,en].png} | 0 234 files changed, 12 insertions(+) rename 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 => ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Day-34_34_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Night-34_35_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Day-35_35_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Night-35_36_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Day-54_54_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Night-54_55_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Day-40_40_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_null_TimelineImageWithCaptionRow-Night-40_41_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-36_36_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Night-36_37_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-37_37_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-37_38_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Day-38_38_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-38_39_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-39_39_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-39_40_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-41_41_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-41_42_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Day-42_42_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_null_TimelineItemLegacyCallInviteView-Night-42_43_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-43_43_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Day-43_43_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-43_44_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemLocationView_null_TimelineItemLocationView-Night-43_44_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Day-44_44_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemPollView_null_TimelineItemPollView-Night-44_45_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-45_45_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-45_46_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Day-46_46_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStateView_null_TimelineItemStateView-Night-46_47_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Day-47_47_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemStickerView_null_TimelineItemStickerView-Night-47_48_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Day-48_48_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemTextView_null_TimelineItemTextView-Night-48_49_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-49_49_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-49_50_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-50_50_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-50_51_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-53_53_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-53_54_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_10,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_11,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_12,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_13,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_14,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_6,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_7,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_8,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-52_52_null_9,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_10,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_11,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_12,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_13,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_14,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_6,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_7,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_8,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-52_53_null_9,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Day-51_51_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_null_TimelineVideoWithCaptionRow-Night-51_52_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-55_55_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-55_56_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Day-56_56_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.reactionsummary_SheetContent_null_SheetContent-Night-56_57_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-58_58_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-58_59_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_6,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Day-57_57_null_7,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_6,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_null_TimelineItemReadReceiptView-Night-57_58_null_7,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-59_59_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-59_59_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-59_60_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-59_60_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-60_60_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-60_61_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-61_61_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-61_61_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-61_62_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-61_62_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-62_62_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-62_63_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-63_63_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-63_64_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-64_64_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-64_65_null,NEXUS_5,1.0,en].png} (100%) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_10,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_11,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_6,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_7,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_8,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Day-25_25_null_9,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_10,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_11,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_5,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_6,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_7,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_8,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemEventRowWithReply_null_TimelineItemEventRowWithReply-Night-25_26_null_9,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Day-27_27_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Night-27_28_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-26_26_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-26_27_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-28_28_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-28_29_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-30_30_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-30_31_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-31_31_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-31_32_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-32_32_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-32_33_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-29_29_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-29_30_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-33_33_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-33_34_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-65_65_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-65_66_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-66_66_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-66_67_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-66_66_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-66_67_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_68_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-67_67_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-68_68_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-67_68_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-68_69_null_8,NEXUS_5,1.0,en].png} (100%) 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-64_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_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-63_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_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-64_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-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-64_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.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-63_64_null,NEXUS_5,1.0,en].png rename to 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 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-26_26_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_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-25_25_null,NEXUS_5,1.0,en].png rename to 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 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-26_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_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-25_26_null,NEXUS_5,1.0,en].png rename to 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 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-65_65_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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_0,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_1,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_2,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Day-65_65_null_3,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_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-Night-66_67_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.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_0,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_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-Night-66_67_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.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_1,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_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-Night-66_67_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.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_2,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_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-Night-66_67_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.focus_FocusRequestStateView_null_FocusRequestStateView-Night-65_66_null_3,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-67_67_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_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_MessagesViewWithTyping_null_MessagesViewWithTyping-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_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-67_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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_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-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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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-67_68_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 From c89f4d15963d4dce4c4d092364b823d2779b0849 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 17:47:35 +0200 Subject: [PATCH 50/53] Fix issue when retrieving Event detail. The wrong method was used. --- .../android/libraries/matrix/impl/timeline/RustTimeline.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 198f231ccb..3ec21e7560 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -517,7 +517,7 @@ class RustTimeline( private fun fetchDetailsForEvent(eventId: EventId): Result { return runCatching { - inner.getEventTimelineItemByEventId(eventId.value) + inner.fetchDetailsForEvent(eventId.value) } } } From 20e376896e56165085107931f07b6205258ac866 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 17:55:31 +0200 Subject: [PATCH 51/53] Handle click on reply navigation. --- .../android/features/messages/impl/timeline/TimelineView.kt | 3 +-- .../impl/timeline/components/TimelineItemEventRow.kt | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) 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 7f76dfe730..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 @@ -108,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 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 1b9655c471..6cece86a58 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 @@ -417,7 +417,6 @@ private fun MessageSenderInformation( private fun MessageEventBubbleContent( event: TimelineItem.Event, onMessageLongClick: () -> Unit, - @Suppress("UNUSED_PARAMETER") inReplyToClick: () -> Unit, onTimestampClicked: () -> Unit, onLinkClicked: (String) -> Unit, @@ -568,8 +567,8 @@ private fun MessageEventBubbleContent( 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) + // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent + .clickable(onClick = inReplyToClick) when (inReplyTo) { is InReplyToDetails.Ready -> { ReplyToContent( From 4707fff833777d3efb6ec0e03cebbd9407a03dec Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 17:57:02 +0200 Subject: [PATCH 52/53] changelog --- changelog.d/2759.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2759.feature 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. From 405a4840a8223f749906a31a9124b179b54f78c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Apr 2024 21:35:07 +0200 Subject: [PATCH 53/53] Fix tests after change on `InReplyTo` classes --- .../components/TimelineItemEventRow.kt | 3 +- .../impl/timeline/model/InReplyToDetails.kt | 20 +++++++--- .../timeline/model/InReplyToDetailTest.kt | 22 +++++----- .../timeline/model/InReplyToMetadataKtTest.kt | 40 +++++++++---------- 4 files changed, 45 insertions(+), 40 deletions(-) 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 6cece86a58..9633cb31bc 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 @@ -92,6 +92,7 @@ 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 @@ -146,7 +147,7 @@ fun TimelineItemEventRow( } fun inReplyToClicked() { - val inReplyToEventId = event.inReplyTo?.eventId ?: return + val inReplyToEventId = event.inReplyTo?.eventId() ?: return inReplyToClick(inReplyToEventId) } 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 41fda8c294..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,17 +28,24 @@ 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 -sealed class InReplyToDetails(val eventId: EventId) { - class Ready( - eventId: EventId, +@Immutable +sealed interface InReplyToDetails { + data class Ready( + val eventId: EventId, val senderId: UserId, val senderProfile: ProfileTimelineDetails, val eventContent: EventContent?, val textContent: String?, - ) : InReplyToDetails(eventId) + ) : InReplyToDetails - class Loading(eventId: EventId) : InReplyToDetails(eventId) - class Error(eventId: EventId, val message: String) : InReplyToDetails(eventId) + 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( 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,