mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +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) {
|
fun handleEvent(event: TimelineEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is TimelineEvent.LoadMore -> {
|
is TimelineEvent.LoadMore -> {
|
||||||
if (event.direction == Timeline.PaginationDirection.FORWARDS && timelineMode is Timeline.Mode.Thread) {
|
if (event.direction == Timeline.PaginationDirection.FORWARDS) {
|
||||||
// Do not paginate forwards in thread mode, as it's not supported
|
if (timelineMode is Timeline.Mode.Thread) {
|
||||||
return
|
// Do not paginate forwards in thread mode, as it's not supported
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (focusRequestState.value.isPending()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
localScope.launch {
|
localScope.launch {
|
||||||
timelineController.paginate(direction = event.direction)
|
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
|
// 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> {
|
private fun calculateServerNamesForRoom(room: JoinedRoom): List<String> {
|
||||||
// If we have no room members, return right ahead
|
// 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
|
@Test
|
||||||
fun `present - focus on known event retrieves the event from cache`() = runTest {
|
fun `present - focus on known event retrieves the event from cache`() = runTest {
|
||||||
val timelineItemIndexer = TimelineItemIndexer()
|
val timelineItemIndexer = TimelineItemIndexer()
|
||||||
@@ -1662,6 +1825,37 @@ class TimelinePresenterTest {
|
|||||||
return awaitItem()
|
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(
|
private fun roomPermissions(
|
||||||
canRedactOther: Boolean = false,
|
canRedactOther: Boolean = false,
|
||||||
canRedactOwn: Boolean = true,
|
canRedactOwn: Boolean = true,
|
||||||
|
|||||||
Reference in New Issue
Block a user