diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index e4ae628263..9f98614fac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/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.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 { return currentTimelineFlow().first().paginate(direction) + .onSuccess { hasReachedEnd -> + if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) { + focusOnLive() + } + } } private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached -> diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt index 400ff8fffd..381bfd8c95 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt +++ b/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 -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) -> Unit): Flow> = callbackFlow { @@ -59,29 +54,6 @@ internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List = - paginationStatusFlow { listener -> - subscribeToBackPaginationStatus(listener) - } - -internal fun Timeline.forwardPaginationStatusFlow(): Flow = - paginationStatusFlow { listener -> - subscribeToForwardPaginationStatus(listener) - } - -private fun paginationStatusFlow(subscriber: suspend (PaginationStatusListener)->TaskHandle): Flow{ - 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 { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 13a8e40161..a3c121fd97 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/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.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 { 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 = inner - .backPaginationStatusFlow() - .map() - .stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)) - - private val forwardPaginationStatus: StateFlow = - 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> = combine( _timelineItems, backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(), diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt deleted file mode 100644 index 482489a517..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineFlows.kt +++ /dev/null @@ -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.map(): Flow = 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 - ) - } -} -