Timeline permalink : automatic focus on live when reaching end of forward pagination (and remove usage of PaginationStatus)
This commit is contained in:
parent
f56ac869ff
commit
e09e23abb0
4 changed files with 36 additions and 90 deletions
|
|
@ -35,11 +35,14 @@ import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
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)
|
@SingleIn(RoomScope::class)
|
||||||
class TimelineController @Inject constructor(
|
class TimelineController @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
|
@ -92,6 +95,11 @@ class TimelineController @Inject constructor(
|
||||||
|
|
||||||
suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
||||||
return currentTimelineFlow().first().paginate(direction)
|
return currentTimelineFlow().first().paginate(direction)
|
||||||
|
.onSuccess { hasReachedEnd ->
|
||||||
|
if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) {
|
||||||
|
focusOnLive()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached ->
|
private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached ->
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.timeline
|
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.cancelAndDestroy
|
||||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
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.Channel
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
|
@ -27,14 +25,11 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
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.Timeline
|
||||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
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
|
||||||
import uniffi.matrix_sdk_ui.PaginationStatus
|
|
||||||
|
|
||||||
internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List<TimelineItem>) -> Unit): Flow<List<TimelineDiff>> =
|
internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List<TimelineItem>) -> Unit): Flow<List<TimelineDiff>> =
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
|
|
@ -59,29 +54,6 @@ internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List<TimelineItem
|
||||||
Timber.d(it, "timelineDiffFlow() failed")
|
Timber.d(it, "timelineDiffFlow() failed")
|
||||||
}.buffer(Channel.UNLIMITED)
|
}.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) {
|
internal suspend fun Timeline.runWithTimelineListenerRegistered(action: suspend () -> Unit) {
|
||||||
val result = addListener(NoOpTimelineListener)
|
val result = addListener(NoOpTimelineListener)
|
||||||
try {
|
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.RoomBeginningPostProcessor
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -38,15 +39,13 @@ import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||||
|
|
@ -58,6 +57,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
||||||
|
|
||||||
private const val INITIAL_MAX_SIZE = 50
|
private const val INITIAL_MAX_SIZE = 50
|
||||||
|
private const val PAGINATION_SIZE = 50
|
||||||
|
|
||||||
class RustTimeline(
|
class RustTimeline(
|
||||||
private val inner: InnerTimeline,
|
private val inner: InnerTimeline,
|
||||||
|
|
@ -105,6 +105,14 @@ class RustTimeline(
|
||||||
timelineItemFactory = timelineItemFactory,
|
timelineItemFactory = timelineItemFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val backPaginationStatus = MutableStateFlow(
|
||||||
|
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val forwardPaginationStatus = MutableStateFlow(
|
||||||
|
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = !isLive)
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
roomCoroutineScope.launch(dispatcher) {
|
roomCoroutineScope.launch(dispatcher) {
|
||||||
inner.timelineDiffFlow { initialList ->
|
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> {
|
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
||||||
initLatch.await()
|
initLatch.await()
|
||||||
return runCatching {
|
return runCatching {
|
||||||
if (!canPaginate(direction)) throw TimelineException.CannotPaginate
|
if (!canPaginate(direction)) throw TimelineException.CannotPaginate
|
||||||
|
updatePaginationStatus(direction) { it.copy(isPaginating = true) }
|
||||||
when (direction) {
|
when (direction) {
|
||||||
Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(50u)
|
Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(PAGINATION_SIZE.toUShort())
|
||||||
Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(50u)
|
Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(PAGINATION_SIZE.toUShort())
|
||||||
}
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
|
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
|
||||||
|
if (error is CancellationException) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
if (error is TimelineException.CannotPaginate) {
|
if (error is TimelineException.CannotPaginate) {
|
||||||
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
|
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
|
||||||
} else {
|
} else {
|
||||||
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
|
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
|
||||||
}
|
}
|
||||||
}.onSuccess {
|
}.onSuccess { hasReachedEnd ->
|
||||||
Timber.v("Success paginating $direction for room ${matrixRoom.roomId}")
|
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(
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
||||||
_timelineItems,
|
_timelineItems,
|
||||||
backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue