From e4174f279245305ddff379e28b675e3c6e8c88ee Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 Apr 2024 21:32:02 +0200 Subject: [PATCH] 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