Browse Source

Improve a bit timeline pagination

misc/jme/add-logging-to-state-machine
ganfra 2 years ago
parent
commit
c9b4cf3232
  1. 21
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt
  2. 63
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt
  3. 62
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemContentView.kt
  4. 4
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemEncryptedView.kt
  5. 4
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemImageView.kt
  6. 4
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemInformativeView.kt
  7. 4
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemRedactedView.kt
  8. 4
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemTextView.kt
  9. 4
      features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/event/TimelineItemUnknownView.kt
  10. 9
      libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimeline.kt
  11. 118
      libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/MatrixTimelineDiffProcessor.kt
  12. 143
      libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/timeline/RustMatrixTimeline.kt
  13. 14
      libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt

21
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.architecture.Presenter
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimeline
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val backPaginationEventLimit = 20 private const val backPaginationEventLimit = 20
@ -55,9 +57,13 @@ class TimelinePresenter @Inject constructor(
.flow() .flow()
.collectAsState(emptyList()) .collectAsState(emptyList())
val paginationState = timeline
.paginationState()
.collectAsState()
fun handleEvents(event: TimelineEvents) { fun handleEvents(event: TimelineEvents) {
when (event) { when (event) {
TimelineEvents.LoadMore -> localCoroutineScope.loadMore() TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
} }
} }
@ -66,6 +72,11 @@ class TimelinePresenter @Inject constructor(
timeline timeline
.timelineItems() .timelineItems()
.onEach(timelineItemsFactory::replaceWith) .onEach(timelineItemsFactory::replaceWith)
.onEach { timelineItems ->
if (timelineItems.isEmpty()) {
loadMore(paginationState.value)
}
}
.launchIn(this) .launchIn(this)
} }
@ -83,7 +94,11 @@ class TimelinePresenter @Inject constructor(
) )
} }
private fun CoroutineScope.loadMore() = launch { private fun CoroutineScope.loadMore(paginationState: MatrixTimeline.PaginationState) = launch {
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize) if (paginationState.canBackPaginate && !paginationState.isBackPaginating) {
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
} else {
Timber.v("Can't back paginate as paginationState = $paginationState")
}
} }
} }

63
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.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState 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.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons 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.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex 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.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.TimelineItemReactionsView
import io.element.android.features.messages.timeline.components.TimelineItemRedactedView import io.element.android.features.messages.timeline.components.event.TimelineItemEventContentView
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.virtual.TimelineItemDaySeparatorView 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.components.virtual.TimelineLoadingMoreIndicator
import io.element.android.features.messages.timeline.model.TimelineItem 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.TimelineItemEventContent
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.timeline.model.event.TimelineItemEventContentProvider
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.virtual.TimelineItemDaySeparatorModel 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.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.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -92,6 +83,11 @@ fun TimelineView(
onMessageClicked: (TimelineItem.Event) -> Unit = {}, onMessageClicked: (TimelineItem.Event) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
) { ) {
fun onReachedLoadMore() {
state.eventSink(TimelineEvents.LoadMore)
}
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
Box(modifier = modifier) { Box(modifier = modifier) {
LazyColumn( LazyColumn(
@ -101,24 +97,23 @@ fun TimelineView(
verticalArrangement = Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
reverseLayout = true reverseLayout = true
) { ) {
items( itemsIndexed(
items = state.timelineItems, items = state.timelineItems,
contentType = { timelineItem -> timelineItem.contentType() }, contentType = { _, timelineItem -> timelineItem.contentType() },
key = { timelineItem -> timelineItem.key() }, key = { _, timelineItem -> timelineItem.key() },
) { timelineItem -> ) { index, timelineItem ->
TimelineItemRow( TimelineItemRow(
timelineItem = timelineItem, timelineItem = timelineItem,
isHighlighted = timelineItem.key() == state.highlightedEventId?.value, isHighlighted = timelineItem.key() == state.highlightedEventId?.value,
onClick = onMessageClicked, onClick = onMessageClicked,
onLongClick = onMessageLongClicked onLongClick = onMessageLongClicked
) )
if (index == state.timelineItems.lastIndex) {
onReachedLoadMore()
}
} }
} }
fun onReachedLoadMore() {
state.eventSink(TimelineEvents.LoadMore)
}
TimelineScrollHelper( TimelineScrollHelper(
lazyListState = lazyListState, lazyListState = lazyListState,
timelineItems = state.timelineItems, timelineItems = state.timelineItems,
@ -231,31 +226,7 @@ fun TimelineItemEventRow(
.widthIn(max = 320.dp) .widthIn(max = 320.dp)
) { ) {
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
when (event.content) { TimelineItemEventContentView(event.content, interactionSource, onClick, onLongClick, contentModifier)
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
)
}
} }
TimelineItemReactionsView( TimelineItemReactionsView(
reactionsState = event.reactionsState, reactionsState = event.reactionsState,

62
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
)
}
}

4
features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemEncryptedView.kt → 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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.Icons
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning

4
features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemImageView.kt → 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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.Box
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio

4
features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemInformativeView.kt → 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer

4
features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemRedactedView.kt → 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete

4
features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemTextView.kt → 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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.SpannableString
import android.text.style.URLSpan import android.text.style.URLSpan

4
features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemUnknownView.kt → 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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.Icons
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info

9
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 io.element.android.libraries.matrix.core.EventId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.matrix.rustcomponents.sdk.TimelineListener import kotlinx.coroutines.flow.StateFlow
interface MatrixTimeline { interface MatrixTimeline {
data class PaginationState(
val isBackPaginating: Boolean,
val canBackPaginate: Boolean
)
fun paginationState(): StateFlow<PaginationState>
fun timelineItems(): Flow<List<MatrixTimelineItem>> fun timelineItems(): Flow<List<MatrixTimelineItem>>
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
fun initialize() fun initialize()

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

143
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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.core.EventId 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 io.element.android.libraries.matrix.util.TaskHandleBag
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.PaginationOptions import org.matrix.rustcomponents.sdk.PaginationOptions
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.SlidingSyncRoom 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.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineListener import org.matrix.rustcomponents.sdk.TimelineListener
import timber.log.Timber import timber.log.Timber
class RustMatrixTimeline( class RustMatrixTimeline(
private val matrixRoom: RustMatrixRoom, private val matrixRoom: MatrixRoom,
private val innerRoom: Room, private val innerRoom: Room,
private val slidingSyncRoom: SlidingSyncRoom, private val slidingSyncRoom: SlidingSyncRoom,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) : MatrixTimeline { ) : MatrixTimeline {
private val innerTimelineListener = object : TimelineListener {
override fun onUpdate(update: TimelineDiff) {
coroutineScope.launch {
updateTimelineItems {
applyDiff(update)
}
}
}
}
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList()) 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() private val listenerTokens = TaskHandleBag()
override fun paginationState(): StateFlow<MatrixTimeline.PaginationState> {
return paginationState
}
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
override fun timelineItems(): Flow<List<MatrixTimelineItem>> { override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return timelineItems.sample(50) 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() { override fun initialize() {
Timber.v("Init timeline for room ${slidingSyncRoom.roomId()}") Timber.v("Init timeline for room ${matrixRoom.roomId}")
coroutineScope.launch { coroutineScope.launch {
matrixRoom.fetchMembers() matrixRoom.fetchMembers()
.onFailure { .onFailure {
Timber.e(it, "Fail to fetch members for room ${slidingSyncRoom.roomId()}") Timber.e(it, "Fail to fetch members for room ${matrixRoom.roomId}")
}.onSuccess { }.onSuccess {
Timber.v("Success fetching members for room ${slidingSyncRoom.roomId()}") Timber.v("Success fetching members for room ${matrixRoom.roomId}")
} }
} }
coroutineScope.launch { coroutineScope.launch {
@ -154,16 +82,18 @@ class RustMatrixTimeline(
result result
.onSuccess { timelineItems -> .onSuccess { timelineItems ->
val matrixTimelineItems = timelineItems.map { it.asMatrixTimelineItem() } val matrixTimelineItems = timelineItems.map { it.asMatrixTimelineItem() }
updateTimelineItems { addAll(matrixTimelineItems) } withContext(coroutineDispatchers.diffUpdateDispatcher) {
this@RustMatrixTimeline.timelineItems.value = matrixTimelineItems
}
} }
.onFailure { .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() { override fun dispose() {
Timber.v("Dispose timeline for room ${slidingSyncRoom.roomId()}") Timber.v("Dispose timeline for room ${matrixRoom.roomId}")
listenerTokens.dispose() listenerTokens.dispose()
} }
@ -181,4 +111,27 @@ class RustMatrixTimeline(
override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit> { override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit> {
return matrixRoom.replyMessage(inReplyToEventId, message) 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
}
}
} }

14
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 io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import org.matrix.rustcomponents.sdk.TimelineListener
class FakeMatrixTimeline : MatrixTimeline { 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>> { override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return emptyFlow() return emptyFlow()
@ -36,8 +44,6 @@ class FakeMatrixTimeline : MatrixTimeline {
return Result.success(Unit) return Result.success(Unit)
} }
override fun addListener(timelineListener: TimelineListener) = Unit
override fun initialize() = Unit override fun initialize() = Unit
override fun dispose() = Unit override fun dispose() = Unit

Loading…
Cancel
Save