Improve a bit timeline pagination
This commit is contained in:
parent
830b8caa3a
commit
c9b4cf3232
13 changed files with 293 additions and 161 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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<PaginationState>
|
||||
|
||||
fun timelineItems(): Flow<List<MatrixTimelineItem>>
|
||||
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
||||
fun initialize()
|
||||
|
|
|
|||
|
|
@ -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<MatrixTimeline.PaginationState>,
|
||||
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>>,
|
||||
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<MatrixTimelineItem>.() -> Unit) =
|
||||
withContext(diffDispatcher) {
|
||||
val mutableTimelineItems = timelineItems.value.toMutableList()
|
||||
block(mutableTimelineItems)
|
||||
timelineItems.value = mutableTimelineItems
|
||||
}
|
||||
|
||||
private fun MutableList<MatrixTimelineItem>.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<MatrixTimelineItem>> =
|
||||
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<MatrixTimeline.PaginationState> {
|
||||
return paginationState
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
|
||||
return timelineItems.sample(50)
|
||||
}
|
||||
|
||||
private fun MutableList<MatrixTimelineItem>.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<Unit> = 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<MatrixTimelineItem>.() -> Unit) =
|
||||
withContext(coroutineDispatchers.diffUpdateDispatcher) {
|
||||
val mutableTimelineItems = timelineItems.value.toMutableList()
|
||||
block(mutableTimelineItems)
|
||||
timelineItems.value = mutableTimelineItems
|
||||
}
|
||||
|
||||
private suspend fun addListener(timelineListener: TimelineListener): Result<List<TimelineItem>> = 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<Unit> {
|
||||
return matrixRoom.replyMessage(inReplyToEventId, message)
|
||||
}
|
||||
|
||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = 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<List<TimelineItem>> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, null)
|
||||
listenerTokens += result.taskHandle
|
||||
result.items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MatrixTimeline.PaginationState> {
|
||||
return paginationState
|
||||
}
|
||||
|
||||
override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue