diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index afd0834ad8..b34af8f5a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -76,11 +76,16 @@ import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds @Composable fun TimelineView( @@ -139,6 +144,10 @@ fun TimelineView( ) } + fun prefetchMoreItems() { + state.eventSink(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)) + } + // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms AnimatedVisibility(visible = true, enter = fadeIn()) { Box(modifier) { @@ -185,9 +194,10 @@ fun TimelineView( onClearFocusRequestState = ::clearFocusRequestState ) - TimelinePrefetchingHelper(lazyListState = lazyListState) { - state.eventSink(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)) - } + TimelinePrefetchingHelper( + lazyListState = lazyListState, + prefetch = ::prefetchMoreItems + ) TimelineScrollHelper( hasAnyEvent = state.hasAnyEvent, @@ -217,7 +227,7 @@ private fun MessageShieldDialog(state: TimelineState) { ) } -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable private fun TimelinePrefetchingHelper( lazyListState: LazyListState, @@ -225,25 +235,25 @@ private fun TimelinePrefetchingHelper( ) { val latestPrefetch by rememberUpdatedState(prefetch) - // We're using snapshot flows for these because using `LaunchedEffect` with `derivedState` doesn't seem to be responsive enough - val firstVisibleItemIndexFlow = snapshotFlow { - lazyListState.firstVisibleItemIndex - } - val layoutInfoFlow = snapshotFlow { - lazyListState.layoutInfo - } - val isScrollingFlow = snapshotFlow { - lazyListState.isScrollInProgress - } + LaunchedEffect(Unit) { + // We're using snapshot flows for these because using `LaunchedEffect` with `derivedState` doesn't seem to be responsive enough + val firstVisibleItemIndexFlow = snapshotFlow { lazyListState.firstVisibleItemIndex } + val layoutInfoFlow = snapshotFlow { lazyListState.layoutInfo } + val isScrollingFlow = snapshotFlow { lazyListState.isScrollInProgress } + // This value changes too frequently, so we debounce it to avoid unnecessary prefetching. It's the equivalent of a conditional 'throttleLatest' + .conflate() + .transform { isScrolling -> + emit(isScrolling) + if (isScrolling) delay(100.milliseconds) + } - LaunchedEffect(latestPrefetch) { val isCloseToStartOfLoadedTimelineFlow = combine(layoutInfoFlow, firstVisibleItemIndexFlow) { layoutInfo, firstVisibleItemIndex -> firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size >= layoutInfo.totalItemsCount - 40 } combine( - isCloseToStartOfLoadedTimelineFlow, - isScrollingFlow, + isCloseToStartOfLoadedTimelineFlow.distinctUntilChanged(), + isScrollingFlow.distinctUntilChanged(), ) { needsPrefetch, isScrolling -> needsPrefetch && isScrolling } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 0c434aef15..fba34f7fab 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -142,7 +142,7 @@ class TimelineViewTest { @Test fun `scrolling near to the start of the loaded items triggers a pre-fetch`() { val eventsRecorder = EventsRecorder() - val items = List(20) { + val items = List(200) { aTimelineItemEvent( eventId = EventId("\$event_$it"), content = aTimelineItemUnknownContent(), @@ -158,7 +158,10 @@ class TimelineViewTest { ), ) - rule.onNodeWithTag("timeline").performScrollToIndex(10) + rule.onNodeWithTag("timeline").performScrollToIndex(180) + + rule.mainClock.advanceTimeBy(1000) + eventsRecorder.assertList( listOf( TimelineEvents.OnScrollFinished(firstIndex = 0),