Timeline permalink : automatic focus on live when reaching end of forward pagination (and remove usage of PaginationStatus)
This commit is contained in:
@@ -35,11 +35,14 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.util.Optional
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
* This controller is responsible of using the right timeline to display messages.
|
||||
* It can be focused on the live timeline or on a detached timeline (focusing an unknown event).
|
||||
*/
|
||||
@SingleIn(RoomScope::class)
|
||||
class TimelineController @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
@@ -92,6 +95,11 @@ class TimelineController @Inject constructor(
|
||||
|
||||
suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
||||
return currentTimelineFlow().first().paginate(direction)
|
||||
.onSuccess { hasReachedEnd ->
|
||||
if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) {
|
||||
focusOnLive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached ->
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.timeline
|
||||
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
@@ -27,14 +25,11 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import org.matrix.rustcomponents.sdk.PaginationStatusListener
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
import org.matrix.rustcomponents.sdk.Timeline
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_ui.PaginationStatus
|
||||
|
||||
internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List<TimelineItem>) -> Unit): Flow<List<TimelineDiff>> =
|
||||
callbackFlow {
|
||||
@@ -59,29 +54,6 @@ internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List<TimelineItem
|
||||
Timber.d(it, "timelineDiffFlow() failed")
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
internal fun Timeline.backPaginationStatusFlow(): Flow<PaginationStatus> =
|
||||
paginationStatusFlow { listener ->
|
||||
subscribeToBackPaginationStatus(listener)
|
||||
}
|
||||
|
||||
internal fun Timeline.forwardPaginationStatusFlow(): Flow<PaginationStatus> =
|
||||
paginationStatusFlow { listener ->
|
||||
subscribeToForwardPaginationStatus(listener)
|
||||
}
|
||||
|
||||
private fun paginationStatusFlow(subscriber: suspend (PaginationStatusListener)->TaskHandle): Flow<PaginationStatus>{
|
||||
return mxCallbackFlow {
|
||||
val listener = object : PaginationStatusListener {
|
||||
override fun onUpdate(status: PaginationStatus) {
|
||||
trySendBlocking(status)
|
||||
}
|
||||
}
|
||||
tryOrNull {
|
||||
subscriber(listener)
|
||||
}
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
}
|
||||
|
||||
internal suspend fun Timeline.runWithTimelineListenerRegistered(action: suspend () -> Unit) {
|
||||
val result = addListener(NoOpTimelineListener)
|
||||
try {
|
||||
|
||||
@@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIn
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -38,15 +39,13 @@ import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
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.getAndUpdate
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
@@ -58,6 +57,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
||||
|
||||
private const val INITIAL_MAX_SIZE = 50
|
||||
private const val PAGINATION_SIZE = 50
|
||||
|
||||
class RustTimeline(
|
||||
private val inner: InnerTimeline,
|
||||
@@ -105,6 +105,14 @@ class RustTimeline(
|
||||
timelineItemFactory = timelineItemFactory,
|
||||
)
|
||||
|
||||
private val backPaginationStatus = MutableStateFlow(
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)
|
||||
)
|
||||
|
||||
private val forwardPaginationStatus = MutableStateFlow(
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = !isLive)
|
||||
)
|
||||
|
||||
init {
|
||||
roomCoroutineScope.launch(dispatcher) {
|
||||
inner.timelineDiffFlow { initialList ->
|
||||
@@ -130,22 +138,34 @@ class RustTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus)->Timeline.PaginationStatus){
|
||||
when (direction) {
|
||||
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.getAndUpdate(update)
|
||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
||||
initLatch.await()
|
||||
return runCatching {
|
||||
if (!canPaginate(direction)) throw TimelineException.CannotPaginate
|
||||
updatePaginationStatus(direction) { it.copy(isPaginating = true) }
|
||||
when (direction) {
|
||||
Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(50u)
|
||||
Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(50u)
|
||||
Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(PAGINATION_SIZE.toUShort())
|
||||
Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(PAGINATION_SIZE.toUShort())
|
||||
}
|
||||
}.onFailure { error ->
|
||||
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
|
||||
if (error is CancellationException) {
|
||||
throw error
|
||||
}
|
||||
if (error is TimelineException.CannotPaginate) {
|
||||
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
|
||||
} else {
|
||||
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
|
||||
}
|
||||
}.onSuccess {
|
||||
Timber.v("Success paginating $direction for room ${matrixRoom.roomId}")
|
||||
}.onSuccess { hasReachedEnd ->
|
||||
updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,20 +184,6 @@ class RustTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
@@ -1,40 +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
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import uniffi.matrix_sdk_ui.PaginationStatus
|
||||
|
||||
fun Flow<PaginationStatus>.map(): Flow<Timeline.PaginationStatus> = map { paginationStatus ->
|
||||
when (paginationStatus) {
|
||||
PaginationStatus.IDLE -> Timeline.PaginationStatus(
|
||||
isPaginating = false,
|
||||
hasMoreToLoad = true
|
||||
)
|
||||
PaginationStatus.PAGINATING -> Timeline.PaginationStatus(
|
||||
isPaginating = true,
|
||||
hasMoreToLoad = true
|
||||
)
|
||||
PaginationStatus.TIMELINE_END_REACHED -> Timeline.PaginationStatus(
|
||||
isPaginating = false,
|
||||
hasMoreToLoad = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user