Timeline permalink : continue to iterate (try a strategy to avoid forward insertion to "auto-scroll")

This commit is contained in:
ganfra 2024-04-23 13:30:55 +02:00
parent ff92551472
commit 0d7cffe400
25 changed files with 599 additions and 218 deletions

View file

@ -43,7 +43,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.timeline.LiveTimeline
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
@ -57,7 +57,7 @@ import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
import io.element.android.libraries.matrix.impl.timeline.RustLiveTimeline
import io.element.android.libraries.matrix.impl.timeline.RustTimeline
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
@ -160,7 +160,7 @@ class RustMatrixRoom(
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
override val liveTimeline = createLiveTimeline(innerTimeline){
override val liveTimeline = createTimeline(innerTimeline, isLive = true){
_syncUpdateFlow.value = systemClock.epochMillis()
}
@ -183,6 +183,12 @@ class RustMatrixRoom(
override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId)
override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline {
return innerRoom.timelineFocusedOnEvent(eventId.value, numContextEvents = 50u).let {inner ->
createTimeline(inner, isLive = false){}
}
}
override fun destroy() {
roomCoroutineScope.cancel()
liveTimeline.close()
@ -745,12 +751,14 @@ class RustMatrixRoom(
}
}
private fun createLiveTimeline(
private fun createTimeline(
timeline: InnerTimeline,
isLive: Boolean = true,
onNewSyncedEvent: () -> Unit = {},
): LiveTimeline {
return RustLiveTimeline(
): Timeline {
return RustTimeline(
isKeyBackupEnabled = isKeyBackupEnabled,
isLive = isLive,
matrixRoom = this,
systemClock = systemClock,
roomCoroutineScope = roomCoroutineScope,

View file

@ -1,20 +0,0 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.timeline
class RustDetachedTimeline {
}

View file

@ -1,55 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.PaginationOptions
import org.matrix.rustcomponents.sdk.Timeline
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem
import timber.log.Timber
import uniffi.matrix_sdk_ui.EventItemOrigin
import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
private const val INITIAL_MAX_SIZE = 50

View file

@ -18,7 +18,6 @@ package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.LiveTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline
@ -27,6 +26,7 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.postprocessor.InvisibleIndicatorPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -58,8 +59,9 @@ import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
private const val INITIAL_MAX_SIZE = 50
class RustLiveTimeline(
class RustTimeline(
private val inner: InnerTimeline,
private val isLive: Boolean,
private val systemClock: SystemClock,
private val roomCoroutineScope: CoroutineScope,
private val isKeyBackupEnabled: Boolean,
@ -68,7 +70,7 @@ class RustLiveTimeline(
private val lastLoginTimestamp: Date?,
private val fetchDetailsForEvent: suspend (EventId) -> Result<Unit>,
private val onNewSyncedEvent: () -> Unit,
) : LiveTimeline {
) : Timeline {
private val initLatch = CompletableDeferred<Unit>()
private val isInit = AtomicBoolean(false)
@ -85,6 +87,7 @@ class RustLiveTimeline(
private val roomBeginningPostProcessor = RoomBeginningPostProcessor()
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
private val invisibleIndicatorPostProcessor = InvisibleIndicatorPostProcessor(isLive)
private val timelineItemFactory = MatrixTimelineItemMapper(
fetchDetailsForEvent = fetchDetailsForEvent,
@ -127,35 +130,59 @@ class RustLiveTimeline(
}
}
override suspend fun paginateBackwards(): Result<Boolean> {
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
initLatch.await()
return runCatching {
if (!canBackPaginate()) throw TimelineException.CannotPaginate
inner.paginateBackwards()
if (!canPaginate(direction)) throw TimelineException.CannotPaginate
when (direction) {
Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(50u)
Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(50u)
}
}.onFailure { error ->
if (error is TimelineException.CannotPaginate) {
Timber.d("Can't paginate backwards on room ${matrixRoom.roomId} with backPaginationStatus: ${backPaginationStatus.value}")
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
} else {
Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}")
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
}
}.onSuccess {
Timber.v("Success back paginating for room ${matrixRoom.roomId}")
Timber.v("Success paginating $direction for room ${matrixRoom.roomId}")
}
}
private fun canBackPaginate(): Boolean {
return isInit.get() && backPaginationStatus.value.canPaginate
private fun canPaginate(direction: Timeline.PaginationDirection): Boolean {
if (!isInit.get()) return false
return when (direction) {
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.value.canPaginate
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate
}
}
override val backPaginationStatus: StateFlow<Timeline.PaginationStatus> = inner
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
return when (direction) {
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
}
}
private val backPaginationStatus: StateFlow<Timeline.PaginationStatus> = inner
.backPaginationStatusFlow()
.map()
.stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true))
private val forwardPaginationStatus: StateFlow<Timeline.PaginationStatus> =
when (isLive) {
true -> MutableStateFlow(Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = false))
false -> inner
.forwardPaginationStatusFlow()
.map()
.stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true))
}
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
_timelineItems,
backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged()
) { timelineItems, hasMoreToLoadBackward ->
backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
forwardPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward ->
timelineItems
.let { items -> encryptedHistoryPostProcessor.process(items) }
.let { items ->
@ -164,7 +191,8 @@ class RustLiveTimeline(
isDm = matrixRoom.isDm,
hasMoreToLoadBackwards = hasMoreToLoadBackward
)
}.let {items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward)}
}.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) }
.let { items -> invisibleIndicatorPostProcessor.process(items) }
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
class InvisibleIndicatorPostProcessor(
private val isLive: Boolean,
) {
private val latestEventIdentifiers: MutableSet<String> = HashSet()
fun process(
items: List<MatrixTimelineItem>,
): List<MatrixTimelineItem> {
if (isLive) {
return items
} else {
return buildList {
items.forEach { item ->
add(item)
if (item is MatrixTimelineItem.Event) {
if (latestEventIdentifiers.contains(item.uniqueId)) {
add(createLatestKnownEventIndicator(item.uniqueId))
}
}
}
items.latestEventIdentifier()?.let { latestEventIdentifier ->
if (latestEventIdentifiers.add(latestEventIdentifier)) {
add(createLatestKnownEventIndicator(latestEventIdentifier))
}
}
}
}
}
private fun createLatestKnownEventIndicator(identifier: String): MatrixTimelineItem {
return MatrixTimelineItem.Virtual(
uniqueId = "latest_known_event_$identifier",
virtual = VirtualTimelineItem.LatestKnownEventIndicator
)
}
private fun List<MatrixTimelineItem>.latestEventIdentifier(): String? {
return findLast {
when (it) {
is MatrixTimelineItem.Event -> true
else -> false
}
}?.let {
(it as MatrixTimelineItem.Event).uniqueId
}
}
private fun List<MatrixTimelineItem>.indexOf(identifier: String): Int {
return indexOfLast {
when (it) {
is MatrixTimelineItem.Event -> {
it.uniqueId == identifier
}
else -> false
}
}
}
}

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.services.toolbox.api.systemclock.SystemClock
@ -24,21 +25,34 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
fun process(
items: List<MatrixTimelineItem>,
hasMoreToLoadBackwards: Boolean,
hasMoreToLoadBackward: Boolean,
hasMoreToLoadForward: Boolean,
): List<MatrixTimelineItem> {
return if (hasMoreToLoadBackwards && !items.hasEncryptionHistoryBanner()){
listOf(
MatrixTimelineItem.Virtual(
uniqueId = "BackwardLoadingIndicator",
virtual = VirtualTimelineItem.LoadingIndicator(
backwards = true,
timestamp = systemClock.epochMillis()
)
val shouldAddBackwardLoadingIndicator = hasMoreToLoadBackward && !items.hasEncryptionHistoryBanner()
val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty()
val currentTimestamp = systemClock.epochMillis()
return buildList {
if (shouldAddBackwardLoadingIndicator) {
val backwardLoadingIndicator = MatrixTimelineItem.Virtual(
uniqueId = "BackwardLoadingIndicator",
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.BACKWARDS,
timestamp = currentTimestamp
)
)
) + items
}else {
items
}
add(backwardLoadingIndicator)
}
addAll(items)
if (shouldAddForwardLoadingIndicator) {
val forwardLoadingIndicator = MatrixTimelineItem.Virtual(
uniqueId = "ForwardLoadingIndicator",
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.FORWARDS,
timestamp = currentTimestamp
)
)
add(forwardLoadingIndicator)
}
}
}
}