Skip to content

Commit 732a4eb

Browse files
authored
Merge pull request #863 from vector-im/feature/fga/timeline_pagination
Feature/fga/timeline pagination
2 parents 978c7c2 + 94cb694 commit 732a4eb

File tree

9 files changed

+61
-30
lines changed

9 files changed

+61
-30
lines changed

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,12 @@ import io.element.android.libraries.architecture.Presenter
3131
import io.element.android.libraries.matrix.api.core.EventId
3232
import io.element.android.libraries.matrix.api.room.MatrixRoom
3333
import io.element.android.libraries.matrix.api.room.MessageEventType
34-
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
3534
import io.element.android.libraries.matrix.ui.room.canSendEventAsState
3635
import kotlinx.collections.immutable.ImmutableList
3736
import kotlinx.coroutines.CoroutineScope
3837
import kotlinx.coroutines.flow.launchIn
3938
import kotlinx.coroutines.flow.onEach
4039
import kotlinx.coroutines.launch
41-
import timber.log.Timber
4240
import javax.inject.Inject
4341

4442
private const val backPaginationEventLimit = 20
@@ -69,7 +67,7 @@ class TimelinePresenter @Inject constructor(
6967

7068
fun handleEvents(event: TimelineEvents) {
7169
when (event) {
72-
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState)
70+
TimelineEvents.LoadMore -> localCoroutineScope.paginateBackwards()
7371
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
7472
is TimelineEvents.OnScrollFinished -> {
7573
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
@@ -89,7 +87,7 @@ class TimelinePresenter @Inject constructor(
8987
.onEach(timelineItemsFactory::replaceWith)
9088
.onEach { timelineItems ->
9189
if (timelineItems.isEmpty()) {
92-
loadMore(paginationState)
90+
paginateBackwards()
9391
}
9492
}
9593
.launchIn(this)
@@ -113,12 +111,8 @@ class TimelinePresenter @Inject constructor(
113111
return null
114112
}
115113

116-
private fun CoroutineScope.loadMore(paginationState: MatrixTimeline.PaginationState) = launch {
117-
if (paginationState.canBackPaginate && !paginationState.isBackPaginating) {
118-
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
119-
} else {
120-
Timber.v("Can't back paginate as paginationState = $paginationState")
121-
}
114+
private fun CoroutineScope.paginateBackwards() = launch {
115+
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
122116
}
123117

124118
private fun CoroutineScope.sendReadReceipt(eventId: EventId) = launch {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import kotlin.random.Random
4141

4242
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
4343
timelineItems = timelineItems,
44-
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, canBackPaginate = true),
44+
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true),
4545
highlightedEventId = null,
4646
canReply = true,
4747
eventSink = {}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ fun TimelineView(
134134
onSwipeToReply = onSwipeToReply,
135135
)
136136
}
137-
if (state.paginationState.canBackPaginate) {
137+
if (state.paginationState.hasMoreToLoadBackwards) {
138138
// Do not use key parameter to avoid wrong positioning
139139
item(contentType = "TimelineLoadingMoreIndicator") {
140140
TimelineLoadingMoreIndicator()

features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,14 @@ class TimelinePresenterTest {
5959
presenter.present()
6060
}.test {
6161
val initialState = awaitItem()
62-
assertThat(initialState.paginationState.canBackPaginate).isTrue()
62+
assertThat(initialState.paginationState.hasMoreToLoadBackwards).isTrue()
6363
assertThat(initialState.paginationState.isBackPaginating).isFalse()
6464
initialState.eventSink.invoke(TimelineEvents.LoadMore)
6565
val inPaginationState = awaitItem()
6666
assertThat(inPaginationState.paginationState.isBackPaginating).isTrue()
67-
assertThat(inPaginationState.paginationState.canBackPaginate).isTrue()
67+
assertThat(inPaginationState.paginationState.hasMoreToLoadBackwards).isTrue()
6868
val postPaginationState = awaitItem()
69-
assertThat(postPaginationState.paginationState.canBackPaginate).isTrue()
69+
assertThat(postPaginationState.paginationState.hasMoreToLoadBackwards).isTrue()
7070
assertThat(postPaginationState.paginationState.isBackPaginating).isFalse()
7171
}
7272
}

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ interface MatrixTimeline {
2424

2525
data class PaginationState(
2626
val isBackPaginating: Boolean,
27-
val canBackPaginate: Boolean
28-
)
27+
val hasMoreToLoadBackwards: Boolean
28+
) {
29+
val canBackPaginate = !isBackPaginating && hasMoreToLoadBackwards
30+
}
2931

3032
val paginationState: StateFlow<PaginationState>
3133
val timelineItems: Flow<List<MatrixTimelineItem>>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.matrix.api.timeline
18+
19+
sealed class TimelineException : Exception() {
20+
object CannotPaginate : TimelineException()
21+
}

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package io.element.android.libraries.matrix.impl.timeline
1818

1919
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
20-
import kotlinx.coroutines.CompletableDeferred
2120
import kotlinx.coroutines.flow.MutableStateFlow
2221
import kotlinx.coroutines.sync.Mutex
2322
import kotlinx.coroutines.sync.withLock
@@ -30,20 +29,16 @@ internal class MatrixTimelineDiffProcessor(
3029
private val timelineItemFactory: MatrixTimelineItemMapper,
3130
) {
3231

33-
private val initLatch = CompletableDeferred<Unit>()
3432
private val mutex = Mutex()
3533

3634
suspend fun postItems(items: List<TimelineItem>) {
3735
updateTimelineItems {
3836
val mappedItems = items.map { it.asMatrixTimelineItem() }
39-
addAll(mappedItems)
37+
addAll(0, mappedItems)
4038
}
41-
initLatch.complete(Unit)
4239
}
4340

4441
suspend fun postDiff(diff: TimelineDiff) {
45-
// Makes sure to process first items before diff.
46-
initLatch.await()
4742
updateTimelineItems {
4843
applyDiff(diff)
4944
}

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import io.element.android.libraries.matrix.api.core.EventId
2020
import io.element.android.libraries.matrix.api.room.MatrixRoom
2121
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
2222
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
23+
import io.element.android.libraries.matrix.api.timeline.TimelineException
2324
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
2425
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
2526
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
2627
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
28+
import kotlinx.coroutines.CompletableDeferred
2729
import kotlinx.coroutines.CoroutineDispatcher
2830
import kotlinx.coroutines.CoroutineScope
2931
import kotlinx.coroutines.FlowPreview
@@ -40,6 +42,9 @@ import org.matrix.rustcomponents.sdk.Room
4042
import org.matrix.rustcomponents.sdk.TimelineDiff
4143
import org.matrix.rustcomponents.sdk.TimelineItem
4244
import timber.log.Timber
45+
import java.util.concurrent.atomic.AtomicBoolean
46+
47+
private const val INITIAL_MAX_SIZE = 50
4348

4449
class RustMatrixTimeline(
4550
roomCoroutineScope: CoroutineScope,
@@ -48,11 +53,14 @@ class RustMatrixTimeline(
4853
private val dispatcher: CoroutineDispatcher,
4954
) : MatrixTimeline {
5055

56+
private val initLatch = CompletableDeferred<Unit>()
57+
private val isInit = AtomicBoolean(false)
58+
5159
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
5260
MutableStateFlow(emptyList())
5361

5462
private val _paginationState = MutableStateFlow(
55-
MatrixTimeline.PaginationState(canBackPaginate = true, isBackPaginating = false)
63+
MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false)
5664
)
5765

5866
private val timelineItemFactory = MatrixTimelineItemMapper(
@@ -77,10 +85,16 @@ class RustMatrixTimeline(
7785
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems.sample(50)
7886

7987
internal suspend fun postItems(items: List<TimelineItem>) {
80-
timelineDiffProcessor.postItems(items)
88+
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
89+
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
90+
timelineDiffProcessor.postItems(it)
91+
}
92+
isInit.set(true)
93+
initLatch.complete(Unit)
8194
}
8295

8396
internal suspend fun postDiff(timelineDiff: TimelineDiff) {
97+
initLatch.await()
8498
timelineDiffProcessor.postDiff(timelineDiff)
8599
}
86100

@@ -90,19 +104,19 @@ class RustMatrixTimeline(
90104
BackPaginationStatus.IDLE -> {
91105
currentPaginationState.copy(
92106
isBackPaginating = false,
93-
canBackPaginate = true
107+
hasMoreToLoadBackwards = true
94108
)
95109
}
96110
BackPaginationStatus.PAGINATING -> {
97111
currentPaginationState.copy(
98112
isBackPaginating = true,
99-
canBackPaginate = true
113+
hasMoreToLoadBackwards = true
100114
)
101115
}
102116
BackPaginationStatus.TIMELINE_START_REACHED -> {
103117
currentPaginationState.copy(
104118
isBackPaginating = false,
105-
canBackPaginate = false
119+
hasMoreToLoadBackwards = false
106120
)
107121
}
108122
}
@@ -117,6 +131,7 @@ class RustMatrixTimeline(
117131

118132
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(dispatcher) {
119133
runCatching {
134+
if (!canBackPaginate()) throw TimelineException.CannotPaginate
120135
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
121136
val paginationOptions = PaginationOptions.UntilNumItems(
122137
eventLimit = requestSize.toUShort(),
@@ -125,12 +140,16 @@ class RustMatrixTimeline(
125140
)
126141
innerRoom.paginateBackwards(paginationOptions)
127142
}.onFailure {
128-
Timber.e(it, "Fail to paginate for room ${matrixRoom.roomId}")
143+
Timber.d(it, "Fail to paginate for room ${matrixRoom.roomId}")
129144
}.onSuccess {
130145
Timber.v("Success back paginating for room ${matrixRoom.roomId}")
131146
}
132147
}
133148

149+
private fun canBackPaginate(): Boolean {
150+
return isInit.get() && paginationState.value.canBackPaginate
151+
}
152+
134153
override suspend fun sendReadReceipt(eventId: EventId) = withContext(dispatcher) {
135154
runCatching {
136155
innerRoom.sendReadReceipt(eventId = eventId.value)

libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.getAndUpdate
2727

2828
class FakeMatrixTimeline(
2929
initialTimelineItems: List<MatrixTimelineItem> = emptyList(),
30-
initialPaginationState: MatrixTimeline.PaginationState = MatrixTimeline.PaginationState(canBackPaginate = true, isBackPaginating = false)
30+
initialPaginationState: MatrixTimeline.PaginationState = MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false)
3131
) : MatrixTimeline {
3232

3333
private val _paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState)

0 commit comments

Comments
 (0)