Timeline : try to get better forward pagination.

This commit is contained in:
ganfra 2024-04-25 23:34:04 +02:00
parent 7ac6e4166a
commit 7b4fa146e5
8 changed files with 119 additions and 101 deletions

View file

@ -31,7 +31,7 @@ import io.element.android.features.messages.impl.timeline.components.virtual.Tim
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemInvisibleIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
@ -50,12 +50,14 @@ fun TimelineItemVirtualRow(
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView()
TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name)
is TimelineItemLoadingIndicatorModel -> {
TimelineLoadingMoreIndicator()
TimelineLoadingMoreIndicator(virtual.model.direction)
LaunchedEffect(virtual.model.timestamp) {
eventSink(TimelineEvents.LoadMore(virtual.model.direction))
}
}
TimelineItemInvisibleIndicatorModel -> Spacer(Modifier)
is TimelineItemLastForwardIndicatorModel -> {
Spacer(modifier = Modifier)
}
}
}
}

View file

@ -17,35 +17,54 @@
package io.element.android.features.messages.impl.timeline.components.virtual
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.matrix.api.timeline.Timeline
@Composable
internal fun TimelineLoadingMoreIndicator(modifier: Modifier = Modifier) {
internal fun TimelineLoadingMoreIndicator(
direction: Timeline.PaginationDirection,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(2.dp),
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
LinearProgressIndicator(modifier = Modifier
.height(1.dp)
.fillMaxWidth()
)
when (direction) {
Timeline.PaginationDirection.FORWARDS -> {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(top = 2.dp)
.height(1.dp)
)
}
Timeline.PaginationDirection.BACKWARDS -> {
CircularProgressIndicator(
strokeWidth = 2.dp,
modifier = Modifier.padding(vertical = 8.dp)
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun TimelineLoadingMoreIndicatorPreview() = ElementPreview {
TimelineLoadingMoreIndicator()
Column {
TimelineLoadingMoreIndicator(Timeline.PaginationDirection.FORWARDS)
TimelineLoadingMoreIndicator(Timeline.PaginationDirection.BACKWARDS)
}
}

View file

@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.virtual
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemInvisibleIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
@ -49,7 +49,7 @@ class TimelineItemVirtualFactory @Inject constructor(
direction = inner.direction,
timestamp = inner.timestamp
)
VirtualTimelineItem.LatestKnownEventIndicator -> TimelineItemInvisibleIndicatorModel
is VirtualTimelineItem.LastForwardIndicator -> TimelineItemLastForwardIndicatorModel
}
}
}

View file

@ -16,6 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.virtual
data object TimelineItemInvisibleIndicatorModel : TimelineItemVirtualModel {
override val type: String = "TimelineItemInvisibleIndicatorModel"
data object TimelineItemLastForwardIndicatorModel: TimelineItemVirtualModel {
override val type: String = "TimelineItemLastForwardIndicatorModel"
}

View file

@ -29,7 +29,7 @@ sealed interface VirtualTimelineItem {
data object RoomBeginning: VirtualTimelineItem
data object LatestKnownEventIndicator: VirtualTimelineItem
data object LastForwardIndicator: VirtualTimelineItem
data class LoadingIndicator(
val direction: Timeline.PaginationDirection,

View file

@ -45,12 +45,11 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.postprocessor.InvisibleIndicatorPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
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
@ -116,7 +115,7 @@ class RustTimeline(
private val roomBeginningPostProcessor = RoomBeginningPostProcessor()
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
private val invisibleIndicatorPostProcessor = InvisibleIndicatorPostProcessor(isLive)
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(isLive)
private val timelineItemFactory = MatrixTimelineItemMapper(
fetchDetailsForEvent = this::fetchDetailsForEvent,
@ -225,7 +224,8 @@ class RustTimeline(
hasMoreToLoadBackwards = hasMoreToLoadBackward
)
}.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) }
.let { items -> invisibleIndicatorPostProcessor.process(items) }
// Keep lastForwardIndicatorsPostProcessor last
.let { items -> lastForwardIndicatorsPostProcessor.process(items) }
}

View file

@ -1,79 +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.postprocessor
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
class InvisibleIndicatorPostProcessor(
private val isLive: Boolean,
) {
private val latestEventIdentifiers: MutableSet<String> = HashSet()
fun process(
items: List<MatrixTimelineItem>,
): List<MatrixTimelineItem> {
if (isLive) {
return items
} else {
return buildList {
items.forEach { item ->
add(item)
if (item is MatrixTimelineItem.Event) {
if (latestEventIdentifiers.contains(item.uniqueId)) {
add(createLatestKnownEventIndicator(item.uniqueId))
}
}
}
items.latestEventIdentifier()?.let { latestEventIdentifier ->
if (latestEventIdentifiers.add(latestEventIdentifier)) {
add(createLatestKnownEventIndicator(latestEventIdentifier))
}
}
}
}
}
private fun createLatestKnownEventIndicator(identifier: String): MatrixTimelineItem {
return MatrixTimelineItem.Virtual(
uniqueId = "latest_known_event_$identifier",
virtual = VirtualTimelineItem.LatestKnownEventIndicator
)
}
private fun List<MatrixTimelineItem>.latestEventIdentifier(): String? {
return findLast {
when (it) {
is MatrixTimelineItem.Event -> true
else -> false
}
}?.let {
(it as MatrixTimelineItem.Event).uniqueId
}
}
private fun List<MatrixTimelineItem>.indexOf(identifier: String): Int {
return indexOfLast {
when (it) {
is MatrixTimelineItem.Event -> {
it.uniqueId == identifier
}
else -> false
}
}
}
}

View file

@ -0,0 +1,76 @@
/*
* 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.postprocessor
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
/**
* This post processor is responsible for adding virtual items to indicate all the previous last forward item.
*/
class LastForwardIndicatorsPostProcessor(
private val isTimelineLive: Boolean,
) {
private val lastForwardIdentifiers = LinkedHashSet<String>()
fun process(
items: List<MatrixTimelineItem>,
): List<MatrixTimelineItem> {
// If the timeline is live, we don't have any last forward indicator to display
if (isTimelineLive) {
return items
} else {
return buildList {
val latestEventIdentifier = items.latestEventIdentifier()
// Remove if it always exists (this should happen only when no new events are added)
lastForwardIdentifiers.remove(latestEventIdentifier)
items.forEach { item ->
add(item)
if (item is MatrixTimelineItem.Event) {
if (lastForwardIdentifiers.contains(item.uniqueId)) {
add(createLastForwardIndicator(item.uniqueId))
}
}
}
// This is important to always add this one at the end of the list so it's used to keep the scroll position.
add(createLastForwardIndicator(latestEventIdentifier))
lastForwardIdentifiers.add(latestEventIdentifier)
}
}
}
}
private fun createLastForwardIndicator(identifier: String): MatrixTimelineItem {
return MatrixTimelineItem.Virtual(
uniqueId = "last_forward_indicator_$identifier",
virtual = VirtualTimelineItem.LastForwardIndicator
)
}
private fun List<MatrixTimelineItem>.latestEventIdentifier(): String {
return findLast {
when (it) {
is MatrixTimelineItem.Event -> true
else -> false
}
}?.let {
(it as MatrixTimelineItem.Event).uniqueId
} ?: "fake_id"
}