mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Merge pull request #7463 from hayaksi1/fix/4460-focused-timeline-pagination
Keep a focused timeline focused until the event has been rendered
This commit is contained in:
+18
-3
@@ -169,9 +169,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)
|
||||
@@ -582,6 +587,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
|
||||
|
||||
+194
@@ -1063,6 +1063,169 @@ 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 room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = room.liveTimeline,
|
||||
)
|
||||
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 room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = room.liveTimeline,
|
||||
)
|
||||
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 room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = room.liveTimeline,
|
||||
)
|
||||
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 - a forward pagination while the focus request is debounced is ignored`() = runTest {
|
||||
val paginateLambda = lambdaRecorder(ensureNeverCalled = true) { _: Timeline.PaginationDirection ->
|
||||
Result.success(true)
|
||||
}
|
||||
val room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = room.liveTimeline,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = 1.seconds))
|
||||
// The focus request has started and is now waiting for the debounce to elapse
|
||||
consumeItemsUntilPredicate { it.focusRequestState is FocusRequestState.Requested }
|
||||
initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS))
|
||||
advanceUntilIdle()
|
||||
assert(paginateLambda).isNeverCalled()
|
||||
assertThat(expectMostRecentItem().isLive).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - a forward pagination while the focused timeline is loading is ignored`() = runTest {
|
||||
val paginateLambda = lambdaRecorder(ensureNeverCalled = true) { _: Timeline.PaginationDirection ->
|
||||
Result.success(true)
|
||||
}
|
||||
val room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = room.liveTimeline,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID))
|
||||
// The focused timeline is being created, the focus request has not resolved yet
|
||||
consumeItemsUntilPredicate { it.focusRequestState is FocusRequestState.Loading }
|
||||
initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS))
|
||||
advanceUntilIdle()
|
||||
assert(paginateLambda).isNeverCalled()
|
||||
assertThat(expectMostRecentItem().isLive).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - a forward pagination after a failed focus request is performed`() = runTest {
|
||||
val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection ->
|
||||
Result.success(false)
|
||||
}
|
||||
val room = aRoomWithDetachedTimeline(
|
||||
paginateLambda = paginateLambda,
|
||||
createTimelineResult = { Result.failure(RuntimeException("An error")) },
|
||||
)
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
timeline = room.liveTimeline,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID))
|
||||
consumeItemsUntilPredicate { it.focusRequestState is FocusRequestState.Failure }
|
||||
initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS))
|
||||
advanceUntilIdle()
|
||||
assert(paginateLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(Timeline.PaginationDirection.FORWARDS))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - a forward pagination in a thread timeline is ignored`() = runTest {
|
||||
val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection ->
|
||||
Result.success(true)
|
||||
}
|
||||
val timeline = FakeTimeline(mode = Timeline.Mode.Thread(A_THREAD_ID)).apply {
|
||||
this.paginateLambda = paginateLambda
|
||||
}
|
||||
val presenter = createTimelinePresenter(timeline = timeline)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS))
|
||||
advanceUntilIdle()
|
||||
assert(paginateLambda).isNeverCalled()
|
||||
// A backward pagination is still performed in a thread timeline
|
||||
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()
|
||||
@@ -1662,6 +1825,37 @@ class TimelinePresenterTest {
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun aRoomWithDetachedTimeline(
|
||||
paginateLambda: (Timeline.PaginationDirection) -> Result<Boolean>,
|
||||
createTimelineResult: () -> Result<Timeline> = { Result.success(aDetachedTimeline(paginateLambda)) },
|
||||
): FakeJoinedRoom {
|
||||
return FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline(timelineItems = flowOf(emptyList())).apply {
|
||||
this.paginateLambda = paginateLambda
|
||||
},
|
||||
createTimelineResult = { createTimelineResult() },
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
threadRootIdForEventResult = { _ -> Result.success(null) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aDetachedTimeline(
|
||||
paginateLambda: (Timeline.PaginationDirection) -> Result<Boolean>,
|
||||
) = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(eventId = AN_EVENT_ID),
|
||||
)
|
||||
)
|
||||
)
|
||||
).apply {
|
||||
this.paginateLambda = paginateLambda
|
||||
}
|
||||
|
||||
private fun roomPermissions(
|
||||
canRedactOther: Boolean = false,
|
||||
canRedactOwn: Boolean = true,
|
||||
|
||||
Reference in New Issue
Block a user