Timeline permalink : continue to iterate (try a strategy to avoid forward insertion to "auto-scroll")
This commit is contained in:
parent
ff92551472
commit
0d7cffe400
25 changed files with 599 additions and 218 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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) }
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue