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
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 ->

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

@ -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 {

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
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(),

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

@ -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