Improve a bit timeline pagination

This commit is contained in:
ganfra 2023-02-24 20:34:32 +01:00
parent 830b8caa3a
commit c9b4cf3232
13 changed files with 293 additions and 161 deletions

View file

@ -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()

View file

@ -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()
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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