mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Keep a focused timeline focused until the event has been rendered
Opening a room from a notification asks the timeline to focus the notified event. When that event is outside the loaded live window the controller creates a detached timeline for it, and a detached timeline always carries a forward loading indicator as its last chronological item. The items list is reversed for display, so that indicator is the bottom-most row and is composed on first layout, which unconditionally emits a forward pagination request nobody asked for. Because a notified event sits close to the live end, that one page reaches the end, and the controller responds by closing the detached timeline. The live window replaces the item list while it is still anchored at the bottom, so the user lands on the newest message and the focus request is discarded. Defer automatic forward pagination while a focus request is still pending. Once the focused event has been rendered the viewport has moved off the indicator row, so only a real scroll back to the live end switches to the live timeline, which is what the controller's existing behaviour is for. Backward pagination and the explicit jump-to-live action are untouched.
This commit is contained in:
+18
-3
@@ -166,9 +166,14 @@ class TimelinePresenter(
|
||||
fun handleEvent(event: TimelineEvent) {
|
||||
when (event) {
|
||||
is TimelineEvent.LoadMore -> {
|
||||
if (event.direction == Timeline.PaginationDirection.FORWARDS && timelineMode is Timeline.Mode.Thread) {
|
||||
// Do not paginate forwards in thread mode, as it's not supported
|
||||
return
|
||||
if (event.direction == Timeline.PaginationDirection.FORWARDS) {
|
||||
if (timelineMode is Timeline.Mode.Thread) {
|
||||
// Do not paginate forwards in thread mode, as it's not supported
|
||||
return
|
||||
}
|
||||
if (focusRequestState.value.isPending()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
localScope.launch {
|
||||
timelineController.paginate(direction = event.direction)
|
||||
@@ -537,6 +542,16 @@ private fun FocusRequestState.onFocusEventRender(): FocusRequestState {
|
||||
}
|
||||
}
|
||||
|
||||
private fun FocusRequestState.isPending(): Boolean {
|
||||
return when (this) {
|
||||
is FocusRequestState.Requested,
|
||||
is FocusRequestState.Loading -> true
|
||||
is FocusRequestState.Success -> !rendered
|
||||
FocusRequestState.None,
|
||||
is FocusRequestState.Failure -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for not having the server names available, get possible server names from the user ids of the room members
|
||||
private fun calculateServerNamesForRoom(room: JoinedRoom): List<String> {
|
||||
// If we have no room members, return right ahead
|
||||
|
||||
+88
@@ -1059,6 +1059,69 @@ class TimelinePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - a forward pagination while the focused event is not rendered yet is ignored`() = runTest {
|
||||
val paginateLambda = lambdaRecorder(ensureNeverCalled = true) { _: Timeline.PaginationDirection ->
|
||||
Result.success(true)
|
||||
}
|
||||
val presenter = createTimelinePresenter(
|
||||
room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID))
|
||||
val focusedState = consumeItemsUntilPredicate { it.focusRequestState == FocusRequestState.Success(AN_EVENT_ID) }.last()
|
||||
assertThat(focusedState.isLive).isFalse()
|
||||
initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS))
|
||||
advanceUntilIdle()
|
||||
assert(paginateLambda).isNeverCalled()
|
||||
assertThat(expectMostRecentItem().isLive).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - a forward pagination is performed once the focused event has been rendered`() = runTest {
|
||||
val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection ->
|
||||
Result.success(true)
|
||||
}
|
||||
val presenter = createTimelinePresenter(
|
||||
room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID))
|
||||
consumeItemsUntilPredicate { it.focusRequestState == FocusRequestState.Success(AN_EVENT_ID) }
|
||||
initialState.eventSink.invoke(TimelineEvent.OnFocusEventRender)
|
||||
initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS))
|
||||
advanceUntilIdle()
|
||||
assert(paginateLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(Timeline.PaginationDirection.FORWARDS))
|
||||
assertThat(expectMostRecentItem().isLive).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - a backward pagination while the focused event is not rendered yet is performed`() = runTest {
|
||||
val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection ->
|
||||
Result.success(false)
|
||||
}
|
||||
val presenter = createTimelinePresenter(
|
||||
room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID))
|
||||
consumeItemsUntilPredicate { it.focusRequestState == FocusRequestState.Success(AN_EVENT_ID) }
|
||||
initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS))
|
||||
advanceUntilIdle()
|
||||
assert(paginateLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(Timeline.PaginationDirection.BACKWARDS))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - focus on known event retrieves the event from cache`() = runTest {
|
||||
val timelineItemIndexer = TimelineItemIndexer()
|
||||
@@ -1552,6 +1615,31 @@ class TimelinePresenterTest {
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun aRoomWithDetachedTimeline(
|
||||
paginateLambda: (Timeline.PaginationDirection) -> Result<Boolean>,
|
||||
): FakeJoinedRoom {
|
||||
val detachedTimeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(eventId = AN_EVENT_ID),
|
||||
)
|
||||
)
|
||||
)
|
||||
).apply {
|
||||
this.paginateLambda = paginateLambda
|
||||
}
|
||||
return FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline(timelineItems = flowOf(emptyList())),
|
||||
createTimelineResult = { Result.success(detachedTimeline) },
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
threadRootIdForEventResult = { _ -> Result.success(null) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun roomPermissions(
|
||||
canRedactOther: Boolean = false,
|
||||
canRedactOwn: Boolean = true,
|
||||
|
||||
Reference in New Issue
Block a user