diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt index aff82273b9..a468072950 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt @@ -28,11 +28,13 @@ import io.element.android.features.messages.timeline.factories.TimelineItemsFact import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.room.MatrixRoom +import io.element.android.libraries.matrix.timeline.MatrixTimeline import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject private const val backPaginationEventLimit = 20 @@ -55,9 +57,13 @@ class TimelinePresenter @Inject constructor( .flow() .collectAsState(emptyList()) + val paginationState = timeline + .paginationState() + .collectAsState() + fun handleEvents(event: TimelineEvents) { when (event) { - TimelineEvents.LoadMore -> localCoroutineScope.loadMore() + TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value) is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId } } @@ -66,6 +72,11 @@ class TimelinePresenter @Inject constructor( timeline .timelineItems() .onEach(timelineItemsFactory::replaceWith) + .onEach { timelineItems -> + if (timelineItems.isEmpty()) { + loadMore(paginationState.value) + } + } .launchIn(this) } @@ -83,7 +94,11 @@ class TimelinePresenter @Inject constructor( ) } - private fun CoroutineScope.loadMore() = launch { - timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize) + private fun CoroutineScope.loadMore(paginationState: MatrixTimeline.PaginationState) = launch { + if (paginationState.canBackPaginate && !paginationState.isBackPaginating) { + timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize) + } else { + Timber.v("Can't back paginate as paginationState = $paginationState") + } } } diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt index cf19fcba21..856a756f7c 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt @@ -34,7 +34,7 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons @@ -54,26 +54,17 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import io.element.android.features.messages.timeline.model.bubble.BubbleState import io.element.android.features.messages.timeline.components.MessageEventBubble -import io.element.android.features.messages.timeline.components.TimelineItemEncryptedView -import io.element.android.features.messages.timeline.components.TimelineItemImageView import io.element.android.features.messages.timeline.components.TimelineItemReactionsView -import io.element.android.features.messages.timeline.components.TimelineItemRedactedView -import io.element.android.features.messages.timeline.components.TimelineItemTextView -import io.element.android.features.messages.timeline.components.TimelineItemUnknownView +import io.element.android.features.messages.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.timeline.components.virtual.TimelineItemDaySeparatorView import io.element.android.features.messages.timeline.components.virtual.TimelineLoadingMoreIndicator import io.element.android.features.messages.timeline.model.TimelineItem -import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent +import io.element.android.features.messages.timeline.model.bubble.BubbleState import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent -import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent -import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent -import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent -import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent +import io.element.android.features.messages.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.timeline.model.virtual.TimelineItemLoadingModel -import io.element.android.features.messages.timeline.model.event.TimelineItemEventContentProvider import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -92,6 +83,11 @@ fun TimelineView( onMessageClicked: (TimelineItem.Event) -> Unit = {}, onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, ) { + + fun onReachedLoadMore() { + state.eventSink(TimelineEvents.LoadMore) + } + val lazyListState = rememberLazyListState() Box(modifier = modifier) { LazyColumn( @@ -101,24 +97,23 @@ fun TimelineView( verticalArrangement = Arrangement.Bottom, reverseLayout = true ) { - items( + itemsIndexed( items = state.timelineItems, - contentType = { timelineItem -> timelineItem.contentType() }, - key = { timelineItem -> timelineItem.key() }, - ) { timelineItem -> + contentType = { _, timelineItem -> timelineItem.contentType() }, + key = { _, timelineItem -> timelineItem.key() }, + ) { index, timelineItem -> TimelineItemRow( timelineItem = timelineItem, isHighlighted = timelineItem.key() == state.highlightedEventId?.value, onClick = onMessageClicked, onLongClick = onMessageLongClicked ) + if (index == state.timelineItems.lastIndex) { + onReachedLoadMore() + } } } - fun onReachedLoadMore() { - state.eventSink(TimelineEvents.LoadMore) - } - TimelineScrollHelper( lazyListState = lazyListState, timelineItems = state.timelineItems, @@ -231,31 +226,7 @@ fun TimelineItemEventRow( .widthIn(max = 320.dp) ) { val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) - when (event.content) { - is TimelineItemEncryptedContent -> TimelineItemEncryptedView( - content = event.content, - modifier = contentModifier - ) - is TimelineItemRedactedContent -> TimelineItemRedactedView( - content = event.content, - modifier = contentModifier - ) - is TimelineItemTextBasedContent -> TimelineItemTextView( - content = event.content, - interactionSource = interactionSource, - modifier = contentModifier, - onTextClicked = onClick, - onTextLongClicked = onLongClick - ) - is TimelineItemUnknownContent -> TimelineItemUnknownView( - content = event.content, - modifier = contentModifier - ) - is TimelineItemImageContent -> TimelineItemImageView( - content = event.content, - modifier = contentModifier - ) - } + TimelineItemEventContentView(event.content, interactionSource, onClick, onLongClick, contentModifier) } TimelineItemReactionsView( reactionsState = event.reactionsState, diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemContentView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemContentView.kt new file mode 100644 index 0000000000..7ab6f14ec6 --- /dev/null +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemContentView.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.timeline.components.event + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent +import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent +import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent +import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent + +@Composable +fun TimelineItemEventContentView( + content: TimelineItemEventContent, + interactionSource: MutableInteractionSource, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier +) { + when (content) { + is TimelineItemEncryptedContent -> TimelineItemEncryptedView( + content = content, + modifier = modifier + ) + is TimelineItemRedactedContent -> TimelineItemRedactedView( + content = content, + modifier = modifier + ) + is TimelineItemTextBasedContent -> TimelineItemTextView( + content = content, + interactionSource = interactionSource, + modifier = modifier, + onTextClicked = onClick, + onTextLongClicked = onLongClick + ) + is TimelineItemUnknownContent -> TimelineItemUnknownView( + content = content, + modifier = modifier + ) + is TimelineItemImageContent -> TimelineItemImageView( + content = content, + modifier = modifier + ) + } +} diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemEncryptedView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemEncryptedView.kt similarity index 97% rename from features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemEncryptedView.kt rename to features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemEncryptedView.kt index 9c1cd929bf..30e0480ca1 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemEncryptedView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemEncryptedView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.timeline.components +package io.element.android.features.messages.timeline.components.event import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemImageView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemImageView.kt similarity index 98% rename from features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemImageView.kt rename to features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemImageView.kt index e9aae61ca9..fcac353e81 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemImageView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemImageView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.timeline.components +package io.element.android.features.messages.timeline.components.event import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemInformativeView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemInformativeView.kt similarity index 98% rename from features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemInformativeView.kt rename to features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemInformativeView.kt index f4ea3d017b..a3718dae22 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemInformativeView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemInformativeView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.timeline.components +package io.element.android.features.messages.timeline.components.event import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemRedactedView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemRedactedView.kt similarity index 97% rename from features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemRedactedView.kt rename to features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemRedactedView.kt index 71dbb8cf55..cbc33d28fb 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemRedactedView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemRedactedView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.timeline.components +package io.element.android.features.messages.timeline.components.event import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemTextView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemTextView.kt similarity index 98% rename from features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemTextView.kt rename to features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemTextView.kt index ddd4055f53..b41ae6a09e 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemTextView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemTextView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.timeline.components +package io.element.android.features.messages.timeline.components.event import android.text.SpannableString import android.text.style.URLSpan diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemUnknownView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemUnknownView.kt similarity index 97% rename from features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemUnknownView.kt rename to features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemUnknownView.kt index cc98359ce7..43acfe43eb 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemUnknownView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemUnknownView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.timeline.components +package io.element.android.features.messages.timeline.components.event import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info diff --git a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimeline.kt b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimeline.kt index 83141b3f6f..83831db882 100644 --- a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimeline.kt +++ b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimeline.kt @@ -18,10 +18,17 @@ package io.element.android.libraries.matrix.timeline import io.element.android.libraries.matrix.core.EventId import kotlinx.coroutines.flow.Flow -import org.matrix.rustcomponents.sdk.TimelineListener +import kotlinx.coroutines.flow.StateFlow interface MatrixTimeline { + data class PaginationState( + val isBackPaginating: Boolean, + val canBackPaginate: Boolean + ) + + fun paginationState(): StateFlow + fun timelineItems(): Flow> suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result fun initialize() diff --git a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimelineDiffProcessor.kt b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimelineDiffProcessor.kt new file mode 100644 index 0000000000..afea46f2ff --- /dev/null +++ b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimelineDiffProcessor.kt @@ -0,0 +1,118 @@ +/* + * 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.timeline + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.TimelineChange +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineListener +import org.matrix.rustcomponents.sdk.VirtualTimelineItem + +internal class MatrixTimelineDiffProcessor( + private val paginationState: MutableStateFlow, + private val timelineItems: MutableStateFlow>, + private val coroutineScope: CoroutineScope, + private val diffDispatcher: CoroutineDispatcher, +) : TimelineListener { + + override fun onUpdate(update: TimelineDiff) { + coroutineScope.launch { + updateTimelineItems { + applyDiff(update) + } + when (val firstItem = timelineItems.value.firstOrNull()) { + is MatrixTimelineItem.Virtual -> updateBackPaginationState(firstItem.virtual) + else -> updateBackPaginationState(null) + } + } + } + + private fun updateBackPaginationState(virtualItem: VirtualTimelineItem?) { + val currentPaginationState = paginationState.value + val newPaginationState = when (virtualItem) { + VirtualTimelineItem.LoadingIndicator -> currentPaginationState.copy( + isBackPaginating = true, + canBackPaginate = true + ) + VirtualTimelineItem.TimelineStart -> currentPaginationState.copy( + isBackPaginating = false, + canBackPaginate = false + ) + else -> currentPaginationState.copy( + isBackPaginating = false, + canBackPaginate = true + ) + } + paginationState.value = newPaginationState + } + + private suspend fun updateTimelineItems(block: MutableList.() -> Unit) = + withContext(diffDispatcher) { + val mutableTimelineItems = timelineItems.value.toMutableList() + block(mutableTimelineItems) + timelineItems.value = mutableTimelineItems + } + + private fun MutableList.applyDiff(diff: TimelineDiff) { + when (diff.change()) { + TimelineChange.APPEND -> { + val items = diff.append()?.map { it.asMatrixTimelineItem() } ?: return + addAll(items) + } + TimelineChange.PUSH_BACK -> { + val item = diff.pushBack()?.asMatrixTimelineItem() ?: return + add(item) + } + TimelineChange.PUSH_FRONT -> { + val item = diff.pushFront()?.asMatrixTimelineItem() ?: return + add(0, item) + } + TimelineChange.SET -> { + val updateAtData = diff.set() ?: return + val item = updateAtData.item.asMatrixTimelineItem() + set(updateAtData.index.toInt(), item) + } + TimelineChange.INSERT -> { + val insertAtData = diff.insert() ?: return + val item = insertAtData.item.asMatrixTimelineItem() + add(insertAtData.index.toInt(), item) + } + TimelineChange.REMOVE -> { + val removeAtData = diff.remove() ?: return + removeAt(removeAtData.toInt()) + } + TimelineChange.RESET -> { + clear() + val items = diff.reset()?.map { it.asMatrixTimelineItem() } ?: return + addAll(items) + } + TimelineChange.POP_FRONT -> { + removeFirstOrNull() + } + TimelineChange.POP_BACK -> { + removeLastOrNull() + } + TimelineChange.CLEAR -> { + clear() + } + } + } +} diff --git a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/RustMatrixTimeline.kt b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/RustMatrixTimeline.kt index c1e13a1ea3..60ccb7420b 100644 --- a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/RustMatrixTimeline.kt @@ -18,135 +18,63 @@ package io.element.android.libraries.matrix.timeline import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.core.EventId -import io.element.android.libraries.matrix.room.RustMatrixRoom +import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.util.TaskHandleBag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.PaginationOptions import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.SlidingSyncRoom -import org.matrix.rustcomponents.sdk.TimelineChange -import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineListener import timber.log.Timber class RustMatrixTimeline( - private val matrixRoom: RustMatrixRoom, + private val matrixRoom: MatrixRoom, private val innerRoom: Room, private val slidingSyncRoom: SlidingSyncRoom, private val coroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixTimeline { - private val innerTimelineListener = object : TimelineListener { - override fun onUpdate(update: TimelineDiff) { - coroutineScope.launch { - updateTimelineItems { - applyDiff(update) - } - } - } - } - private val timelineItems: MutableStateFlow> = MutableStateFlow(emptyList()) + private val paginationState = MutableStateFlow( + MatrixTimeline.PaginationState(canBackPaginate = true, isBackPaginating = false) + ) + + private val innerTimelineListener = MatrixTimelineDiffProcessor( + paginationState = paginationState, + timelineItems = timelineItems, + coroutineScope = coroutineScope, + diffDispatcher = coroutineDispatchers.diffUpdateDispatcher + ) + private val listenerTokens = TaskHandleBag() + override fun paginationState(): StateFlow { + return paginationState + } @OptIn(FlowPreview::class) override fun timelineItems(): Flow> { return timelineItems.sample(50) } - private fun MutableList.applyDiff(diff: TimelineDiff) { - when (diff.change()) { - TimelineChange.APPEND -> { - val items = diff.append()?.map { it.asMatrixTimelineItem() } ?: return - addAll(items) - } - TimelineChange.PUSH_BACK -> { - val item = diff.pushBack()?.asMatrixTimelineItem() ?: return - add(item) - } - TimelineChange.PUSH_FRONT -> { - val item = diff.pushFront()?.asMatrixTimelineItem() ?: return - add(0, item) - } - TimelineChange.SET -> { - val updateAtData = diff.set() ?: return - val item = updateAtData.item.asMatrixTimelineItem() - set(updateAtData.index.toInt(), item) - } - TimelineChange.INSERT -> { - val insertAtData = diff.insert() ?: return - val item = insertAtData.item.asMatrixTimelineItem() - add(insertAtData.index.toInt(), item) - } - TimelineChange.REMOVE -> { - val removeAtData = diff.remove() ?: return - removeAt(removeAtData.toInt()) - } - TimelineChange.RESET -> { - clear() - val items = diff.reset()?.map { it.asMatrixTimelineItem() } ?: return - addAll(items) - } - TimelineChange.POP_FRONT -> { - removeFirstOrNull() - } - TimelineChange.POP_BACK -> { - removeLastOrNull() - } - TimelineChange.CLEAR -> { - clear() - } - } - } - - override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result = withContext(coroutineDispatchers.io) { - runCatching { - Timber.v("Start back paginating for room ${slidingSyncRoom.roomId()} ") - val paginationOptions = PaginationOptions.UntilNumItems( - eventLimit = requestSize.toUShort(), - items = untilNumberOfItems.toUShort() - ) - innerRoom.paginateBackwards(paginationOptions) - }.onFailure { - Timber.e(it, "Fail to paginate for room ${slidingSyncRoom.roomId()}") - }.onSuccess { - Timber.v("Success back paginating for room ${slidingSyncRoom.roomId()}") - } - } - - private suspend fun updateTimelineItems(block: MutableList.() -> Unit) = - withContext(coroutineDispatchers.diffUpdateDispatcher) { - val mutableTimelineItems = timelineItems.value.toMutableList() - block(mutableTimelineItems) - timelineItems.value = mutableTimelineItems - } - - private suspend fun addListener(timelineListener: TimelineListener): Result> = withContext(coroutineDispatchers.computation) { - runCatching { - val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, null) - listenerTokens += result.taskHandle - result.items - } - } - override fun initialize() { - Timber.v("Init timeline for room ${slidingSyncRoom.roomId()}") + Timber.v("Init timeline for room ${matrixRoom.roomId}") coroutineScope.launch { matrixRoom.fetchMembers() .onFailure { - Timber.e(it, "Fail to fetch members for room ${slidingSyncRoom.roomId()}") + Timber.e(it, "Fail to fetch members for room ${matrixRoom.roomId}") }.onSuccess { - Timber.v("Success fetching members for room ${slidingSyncRoom.roomId()}") + Timber.v("Success fetching members for room ${matrixRoom.roomId}") } } coroutineScope.launch { @@ -154,16 +82,18 @@ class RustMatrixTimeline( result .onSuccess { timelineItems -> val matrixTimelineItems = timelineItems.map { it.asMatrixTimelineItem() } - updateTimelineItems { addAll(matrixTimelineItems) } + withContext(coroutineDispatchers.diffUpdateDispatcher) { + this@RustMatrixTimeline.timelineItems.value = matrixTimelineItems + } } .onFailure { - Timber.e("Failed adding timeline listener on room with identifier: ${slidingSyncRoom.roomId()})") + Timber.e("Failed adding timeline listener on room with identifier: ${matrixRoom.roomId})") } } } override fun dispose() { - Timber.v("Dispose timeline for room ${slidingSyncRoom.roomId()}") + Timber.v("Dispose timeline for room ${matrixRoom.roomId}") listenerTokens.dispose() } @@ -181,4 +111,27 @@ class RustMatrixTimeline( override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result { return matrixRoom.replyMessage(inReplyToEventId, message) } + + override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result = withContext(coroutineDispatchers.io) { + runCatching { + Timber.v("Start back paginating for room ${matrixRoom.roomId} ") + val paginationOptions = PaginationOptions.UntilNumItems( + eventLimit = requestSize.toUShort(), + items = untilNumberOfItems.toUShort() + ) + innerRoom.paginateBackwards(paginationOptions) + }.onFailure { + Timber.e(it, "Fail to paginate for room ${matrixRoom.roomId}") + }.onSuccess { + Timber.v("Success back paginating for room ${matrixRoom.roomId}") + } + } + + private suspend fun addListener(timelineListener: TimelineListener): Result> = withContext(coroutineDispatchers.io) { + runCatching { + val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, null) + listenerTokens += result.taskHandle + result.items + } + } } diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt index 18127dcd2d..48ce246c03 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt @@ -21,11 +21,19 @@ import io.element.android.libraries.matrix.timeline.MatrixTimeline import io.element.android.libraries.matrix.timeline.MatrixTimelineItem import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.emptyFlow -import org.matrix.rustcomponents.sdk.TimelineListener class FakeMatrixTimeline : MatrixTimeline { - override var callback: MatrixTimeline.Callback? = null + + private val paginationState = MutableStateFlow( + MatrixTimeline.PaginationState(canBackPaginate = true, isBackPaginating = false) + ) + + override fun paginationState(): StateFlow { + return paginationState + } override fun timelineItems(): Flow> { return emptyFlow() @@ -36,8 +44,6 @@ class FakeMatrixTimeline : MatrixTimeline { return Result.success(Unit) } - override fun addListener(timelineListener: TimelineListener) = Unit - override fun initialize() = Unit override fun dispose() = Unit