Browse Source

Timeline permalink : automatic focus on live when reaching end of forward pagination (and remove usage of PaginationStatus)

pull/2759/head
ganfra 5 months ago
parent
commit
b1dd225648
  1. 10
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt
  2. 28
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt
  3. 48
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
  4. 40
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt

10
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt

@ -35,11 +35,14 @@ import kotlinx.coroutines.flow.first @@ -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( @@ -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 ->

28
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt

@ -16,10 +16,8 @@ @@ -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 @@ -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 @@ -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 {

48
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt

@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIn @@ -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 @@ -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 @@ -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( @@ -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( @@ -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( @@ -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(),

40
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt

@ -1,40 +0,0 @@ @@ -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…
Cancel
Save