Fix loading initial items of non-live timelines (#6598)

This was done automatically by the SDK in the past by returning a `Reset` timeline update, but this behaviour changed and now we have to do it.
This commit is contained in:
Jorge Martin Espinosa 2026-04-16 16:19:29 +02:00 committed by GitHub
parent 67c0e4c140
commit c349f74ce8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 48 additions and 4 deletions

View file

@ -84,6 +84,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import timber.log.Timber
@ -262,11 +263,16 @@ private fun TimelinePrefetchingHelper(
firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size >= layoutInfo.totalItemsCount - 40
}
// If we have no timeline items, we need to back paginate to load some messages. This usually happens on all timelines except for live ones.
// This automatic pagination was previously done by the SDK, and we received a `Reset` update, but now we need to do it ourselves.
val isEmptyTimelineFlow = layoutInfoFlow.map { it.totalItemsCount == 0 }
combine(
isCloseToStartOfLoadedTimelineFlow.distinctUntilChanged(),
isScrollingFlow.distinctUntilChanged(),
) { needsPrefetch, isScrolling ->
needsPrefetch && isScrolling
isEmptyTimelineFlow,
) { needsPrefetch, isScrolling, isEmptyAndNeedsBackPagination ->
isEmptyAndNeedsBackPagination || needsPrefetch && isScrolling
}
.distinctUntilChanged()
.collectLatest { needsPrefetch ->

View file

@ -522,6 +522,9 @@ class MessagesViewTest {
rule.setMessagesView(
state = stateWithActionListState,
)
// Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear()
val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice")
rule.onNodeWithText(verifiedUserSendFailure).performClick()
// Give time for the close animation to complete
@ -585,6 +588,9 @@ class MessagesViewTest {
),
)
rule.setMessagesView(state = state)
// Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear()
rule.onNodeWithText("This is a pinned message").performClick()
eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds))
}
@ -601,6 +607,9 @@ class MessagesViewTest {
timelineState = aTimelineState(eventSink = eventsRecorder)
)
rule.setMessagesView(state = state)
// Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear()
val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action)
// The bottomsheet subcompose seems to make the node to appear twice
rule.onAllNodesWithText(text).onFirst().performClick()

View file

@ -67,24 +67,31 @@ class TimelineViewTest {
@Test
fun `reaching the end of the timeline does not send a LoadMore event`() {
val eventsRecorder = EventsRecorder<TimelineEvent>(expectEvents = false)
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
eventSink = eventsRecorder,
),
)
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
}
@Test
fun `scroll to bottom on live timeline does not emit the Event`() {
val eventsRecorder = EventsRecorder<TimelineEvent>(expectEvents = false)
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = true,
eventSink = eventsRecorder,
),
forceJumpToBottomVisibility = true,
)
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear()
val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom)
rule.onNodeWithContentDescription(contentDescription).performClick()
}
@ -94,15 +101,33 @@ class TimelineViewTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = false,
eventSink = eventsRecorder,
),
)
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear()
val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom)
rule.onNodeWithContentDescription(contentDescription).performClick()
eventsRecorder.assertSingle(TimelineEvent.JumpToLive)
}
@Test
fun `an empty timeline triggers a prefetch`() {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(),
eventSink = eventsRecorder,
),
)
eventsRecorder.assertSingle(TimelineEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS))
}
@Test
fun `show shield dialog`() {
val eventsRecorder = EventsRecorder<TimelineEvent>()
@ -133,11 +158,15 @@ class TimelineViewTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = false,
eventSink = eventsRecorder,
messageShield = aCriticalShield(),
),
)
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear()
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog)
}