Improve a bit timeline pagination

This commit is contained in:
ganfra 2023-02-24 20:34:32 +01:00
parent 830b8caa3a
commit c9b4cf3232
13 changed files with 293 additions and 161 deletions

View file

@ -28,11 +28,13 @@ import io.element.android.features.messages.timeline.factories.TimelineItemsFact
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimeline
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
private const val backPaginationEventLimit = 20
@ -55,9 +57,13 @@ class TimelinePresenter @Inject constructor(
.flow()
.collectAsState(emptyList())
val paginationState = timeline
.paginationState()
.collectAsState()
fun handleEvents(event: TimelineEvents) {
when (event) {
TimelineEvents.LoadMore -> localCoroutineScope.loadMore()
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
}
}
@ -66,6 +72,11 @@ class TimelinePresenter @Inject constructor(
timeline
.timelineItems()
.onEach(timelineItemsFactory::replaceWith)
.onEach { timelineItems ->
if (timelineItems.isEmpty()) {
loadMore(paginationState.value)
}
}
.launchIn(this)
}
@ -83,7 +94,11 @@ class TimelinePresenter @Inject constructor(
)
}
private fun CoroutineScope.loadMore() = launch {
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
private fun CoroutineScope.loadMore(paginationState: MatrixTimeline.PaginationState) = launch {
if (paginationState.canBackPaginate && !paginationState.isBackPaginating) {
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
} else {
Timber.v("Can't back paginate as paginationState = $paginationState")
}
}
}

View file

@ -34,7 +34,7 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
@ -54,26 +54,17 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.element.android.features.messages.timeline.model.bubble.BubbleState
import io.element.android.features.messages.timeline.components.MessageEventBubble
import io.element.android.features.messages.timeline.components.TimelineItemEncryptedView
import io.element.android.features.messages.timeline.components.TimelineItemImageView
import io.element.android.features.messages.timeline.components.TimelineItemReactionsView
import io.element.android.features.messages.timeline.components.TimelineItemRedactedView
import io.element.android.features.messages.timeline.components.TimelineItemTextView
import io.element.android.features.messages.timeline.components.TimelineItemUnknownView
import io.element.android.features.messages.timeline.components.event.TimelineItemEventContentView
import io.element.android.features.messages.timeline.components.virtual.TimelineItemDaySeparatorView
import io.element.android.features.messages.timeline.components.virtual.TimelineLoadingMoreIndicator
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.timeline.model.bubble.BubbleState
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContentProvider
import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.timeline.model.virtual.TimelineItemLoadingModel
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContentProvider
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -92,6 +83,11 @@ fun TimelineView(
onMessageClicked: (TimelineItem.Event) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
) {
fun onReachedLoadMore() {
state.eventSink(TimelineEvents.LoadMore)
}
val lazyListState = rememberLazyListState()
Box(modifier = modifier) {
LazyColumn(
@ -101,24 +97,23 @@ fun TimelineView(
verticalArrangement = Arrangement.Bottom,
reverseLayout = true
) {
items(
itemsIndexed(
items = state.timelineItems,
contentType = { timelineItem -> timelineItem.contentType() },
key = { timelineItem -> timelineItem.key() },
) { timelineItem ->
contentType = { _, timelineItem -> timelineItem.contentType() },
key = { _, timelineItem -> timelineItem.key() },
) { index, timelineItem ->
TimelineItemRow(
timelineItem = timelineItem,
isHighlighted = timelineItem.key() == state.highlightedEventId?.value,
onClick = onMessageClicked,
onLongClick = onMessageLongClicked
)
if (index == state.timelineItems.lastIndex) {
onReachedLoadMore()
}
}
}
fun onReachedLoadMore() {
state.eventSink(TimelineEvents.LoadMore)
}
TimelineScrollHelper(
lazyListState = lazyListState,
timelineItems = state.timelineItems,
@ -231,31 +226,7 @@ fun TimelineItemEventRow(
.widthIn(max = 320.dp)
) {
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
when (event.content) {
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
content = event.content,
modifier = contentModifier
)
is TimelineItemRedactedContent -> TimelineItemRedactedView(
content = event.content,
modifier = contentModifier
)
is TimelineItemTextBasedContent -> TimelineItemTextView(
content = event.content,
interactionSource = interactionSource,
modifier = contentModifier,
onTextClicked = onClick,
onTextLongClicked = onLongClick
)
is TimelineItemUnknownContent -> TimelineItemUnknownView(
content = event.content,
modifier = contentModifier
)
is TimelineItemImageContent -> TimelineItemImageView(
content = event.content,
modifier = contentModifier
)
}
TimelineItemEventContentView(event.content, interactionSource, onClick, onLongClick, contentModifier)
}
TimelineItemReactionsView(
reactionsState = event.reactionsState,

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2023 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.features.messages.timeline.components.event
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
@Composable
fun TimelineItemEventContentView(
content: TimelineItemEventContent,
interactionSource: MutableInteractionSource,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier
) {
when (content) {
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
content = content,
modifier = modifier
)
is TimelineItemRedactedContent -> TimelineItemRedactedView(
content = content,
modifier = modifier
)
is TimelineItemTextBasedContent -> TimelineItemTextView(
content = content,
interactionSource = interactionSource,
modifier = modifier,
onTextClicked = onClick,
onTextLongClicked = onLongClick
)
is TimelineItemUnknownContent -> TimelineItemUnknownView(
content = content,
modifier = modifier
)
is TimelineItemImageContent -> TimelineItemImageView(
content = content,
modifier = modifier
)
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
package io.element.android.features.messages.timeline.components.event
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
package io.element.android.features.messages.timeline.components.event
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
package io.element.android.features.messages.timeline.components.event
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
package io.element.android.features.messages.timeline.components.event
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
package io.element.android.features.messages.timeline.components.event
import android.text.SpannableString
import android.text.style.URLSpan

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
package io.element.android.features.messages.timeline.components.event
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info