mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Show jump-to-unread when read marker is outside the loaded window
When the m.fully_read marker event is older than the loaded timeline window, no virtual ReadMarker item is inserted and the chevron-up FAB silently no-ops. Surfaces the SDK's new fully_read_event_id on RoomInfo and routes the tap through the existing FocusOnEvent flow so the FAB appears in catch-up scenarios and lands the user on the marker. Bumps matrix-rust-components-kotlin to 26.05.20 to pick up the FFI field; absorbs the ClientBuildException.InvalidRawKey and suspend SpaceRoomList.rooms / subscribeToRoomUpdate API breaks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-11
@@ -291,20 +291,32 @@ class TimelinePresenter(
|
||||
// without changing the list length, e.g. when [markRoomAsFullyRead] is sent while at the
|
||||
// bottom of the room.
|
||||
//
|
||||
// Limitation: when the read marker is outside the loaded window (gaps, pagination), this
|
||||
// returns null and the jump-to-unread button stays hidden. Proper fix needs an SDK
|
||||
// accessor for the m.fully_read marker plus FocusedOnEvent navigation on click; gated
|
||||
// behind FeatureFlags.JumpToUnread until that lands.
|
||||
val readMarkerIndex = remember { mutableStateOf<Int?>(null) }
|
||||
LaunchedEffect(timelineItems, displayJumpToUnread) {
|
||||
// The state has three shapes:
|
||||
// - InWindow: the SDK has materialised a virtual ReadMarker item in the loaded window;
|
||||
// tapping the FAB smoothly scrolls to its index.
|
||||
// - OutOfWindow: the marker event is older than the loaded window, so the SDK gives us
|
||||
// only the event id via RoomInfo.fullyReadEventId; tapping triggers a focused-event
|
||||
// load via the existing TimelineEvent.FocusOnEvent path.
|
||||
// - Hidden: feature flag off, no marker, caught-up (marker loaded but no virtual item),
|
||||
// or initial load (no items yet).
|
||||
val jumpToUnread = remember { mutableStateOf<JumpToUnreadState>(JumpToUnreadState.Hidden) }
|
||||
LaunchedEffect(timelineItems, displayJumpToUnread, roomInfo.fullyReadEventId) {
|
||||
if (!displayJumpToUnread) {
|
||||
readMarkerIndex.value = null
|
||||
jumpToUnread.value = JumpToUnreadState.Hidden
|
||||
return@LaunchedEffect
|
||||
}
|
||||
val items = timelineItems
|
||||
readMarkerIndex.value = withContext(dispatchers.computation) {
|
||||
items.indexOfFirst { (it as? TimelineItem.Virtual)?.model is TimelineItemReadMarkerModel }
|
||||
.takeIf { it >= 0 }
|
||||
val fullyReadEventId = roomInfo.fullyReadEventId
|
||||
jumpToUnread.value = withContext(dispatchers.computation) {
|
||||
val markerIndex = items.indexOfFirst {
|
||||
(it as? TimelineItem.Virtual)?.model is TimelineItemReadMarkerModel
|
||||
}
|
||||
when {
|
||||
markerIndex >= 0 -> JumpToUnreadState.InWindow(markerIndex)
|
||||
fullyReadEventId != null && items.isNotEmpty() && !timelineItemIndexer.isKnown(fullyReadEventId) ->
|
||||
JumpToUnreadState.OutOfWindow(fullyReadEventId)
|
||||
else -> JumpToUnreadState.Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +370,7 @@ class TimelinePresenter(
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
displayFloatingDateBadge = displayFloatingDateBadge,
|
||||
displayJumpToUnread = displayJumpToUnread,
|
||||
readMarkerIndex = readMarkerIndex.value,
|
||||
jumpToUnread = jumpToUnread.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+16
-1
@@ -36,7 +36,7 @@ data class TimelineState(
|
||||
val displayThreadSummaries: Boolean,
|
||||
val displayFloatingDateBadge: Boolean,
|
||||
val displayJumpToUnread: Boolean,
|
||||
val readMarkerIndex: Int?,
|
||||
val jumpToUnread: JumpToUnreadState,
|
||||
val eventSink: (TimelineEvent) -> Unit,
|
||||
) {
|
||||
private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event
|
||||
@@ -85,3 +85,18 @@ data class TimelineRoomInfo(
|
||||
val typingNotificationState: TypingNotificationState,
|
||||
val predecessorRoom: PredecessorRoom?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether the jump-to-unread FAB should be shown, and if so, how tapping it
|
||||
* should bring the user to the read marker.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface JumpToUnreadState {
|
||||
data object Hidden : JumpToUnreadState
|
||||
|
||||
/** The read marker is materialised at [index] in the loaded timeline — smooth scroll to it. */
|
||||
data class InWindow(val index: Int) : JumpToUnreadState
|
||||
|
||||
/** The read marker event is older than the loaded window — load it via focused-event navigation. */
|
||||
data class OutOfWindow(val eventId: EventId) : JumpToUnreadState
|
||||
}
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ fun aTimelineState(
|
||||
displayThreadSummaries: Boolean = false,
|
||||
displayFloatingDateBadge: Boolean = false,
|
||||
displayJumpToUnread: Boolean = false,
|
||||
readMarkerIndex: Int? = null,
|
||||
jumpToUnread: JumpToUnreadState = JumpToUnreadState.Hidden,
|
||||
newEventState: NewEventState = NewEventState.None,
|
||||
eventSink: (TimelineEvent) -> Unit = {},
|
||||
): TimelineState {
|
||||
@@ -83,7 +83,7 @@ fun aTimelineState(
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
displayFloatingDateBadge = displayFloatingDateBadge,
|
||||
displayJumpToUnread = displayJumpToUnread,
|
||||
readMarkerIndex = readMarkerIndex,
|
||||
jumpToUnread = jumpToUnread,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
|
||||
+26
-10
@@ -155,6 +155,10 @@ fun TimelineView(
|
||||
state.eventSink(TimelineEvent.MarkAllAsRead)
|
||||
}
|
||||
|
||||
fun onFocusOnEvent(eventId: EventId) {
|
||||
state.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
||||
val view = LocalView.current
|
||||
@@ -240,11 +244,12 @@ fun TimelineView(
|
||||
isLive = state.isLive,
|
||||
focusRequestState = state.focusRequestState,
|
||||
displayJumpToUnread = state.displayJumpToUnread,
|
||||
readMarkerIndex = state.readMarkerIndex,
|
||||
jumpToUnread = state.jumpToUnread,
|
||||
onScrollFinishAt = ::onScrollFinishAt,
|
||||
onJumpToLive = ::onJumpToLive,
|
||||
onFocusEventRender = ::onFocusEventRender,
|
||||
onMarkAllAsRead = ::onMarkAllAsRead,
|
||||
onFocusOnEvent = ::onFocusOnEvent,
|
||||
)
|
||||
|
||||
if (state.displayFloatingDateBadge && useReverseLayout) {
|
||||
@@ -326,11 +331,12 @@ private fun BoxScope.TimelineScrollHelper(
|
||||
forceJumpToReadMarkerVisibility: Boolean,
|
||||
focusRequestState: FocusRequestState,
|
||||
displayJumpToUnread: Boolean,
|
||||
readMarkerIndex: Int?,
|
||||
jumpToUnread: JumpToUnreadState,
|
||||
onScrollFinishAt: (Int) -> Unit,
|
||||
onJumpToLive: () -> Unit,
|
||||
onFocusEventRender: () -> Unit,
|
||||
onMarkAllAsRead: () -> Unit,
|
||||
onFocusOnEvent: (EventId) -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } }
|
||||
@@ -339,12 +345,19 @@ private fun BoxScope.TimelineScrollHelper(
|
||||
lazyListState.firstVisibleItemIndex < 3 && isLive
|
||||
}
|
||||
}
|
||||
val isJumpToUnreadVisible by remember(readMarkerIndex, forceJumpToReadMarkerVisibility) {
|
||||
val isJumpToUnreadVisible by remember(jumpToUnread, forceJumpToReadMarkerVisibility) {
|
||||
derivedStateOf {
|
||||
if (forceJumpToReadMarkerVisibility) return@derivedStateOf true
|
||||
val markerIndex = readMarkerIndex ?: return@derivedStateOf false
|
||||
val lastVisibleIndex = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: return@derivedStateOf false
|
||||
markerIndex > lastVisibleIndex
|
||||
when (val jtu = jumpToUnread) {
|
||||
JumpToUnreadState.Hidden -> false
|
||||
// Marker is outside the loaded window — we have no on-screen anchor, so just show.
|
||||
is JumpToUnreadState.OutOfWindow -> true
|
||||
// Marker is in the loaded window — hide once it's scrolled into the visible range.
|
||||
is JumpToUnreadState.InWindow -> {
|
||||
val lastVisibleIndex = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: return@derivedStateOf false
|
||||
jtu.index > lastVisibleIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val isJumpToBottomVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive
|
||||
@@ -375,9 +388,12 @@ private fun BoxScope.TimelineScrollHelper(
|
||||
}
|
||||
|
||||
fun jumpToReadMarker() {
|
||||
val markerIndex = readMarkerIndex ?: return
|
||||
coroutineScope.launch {
|
||||
lazyListState.animateScrollToItemCenter(markerIndex)
|
||||
when (val jtu = jumpToUnread) {
|
||||
JumpToUnreadState.Hidden -> Unit
|
||||
is JumpToUnreadState.InWindow -> coroutineScope.launch {
|
||||
lazyListState.animateScrollToItemCenter(jtu.index)
|
||||
}
|
||||
is JumpToUnreadState.OutOfWindow -> onFocusOnEvent(jtu.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,7 +646,7 @@ private fun TimelineViewWithReadMarker(
|
||||
// Index points past the loaded items, mirroring the real-world state the FAB
|
||||
// represents: the user has scrolled past the read marker, so it's no longer in
|
||||
// view. The actual scroll target doesn't matter for a static preview.
|
||||
readMarkerIndex = if (hasUnreadAbove) timelineItems.size else null,
|
||||
jumpToUnread = if (hasUnreadAbove) JumpToUnreadState.InWindow(timelineItems.size) else JumpToUnreadState.Hidden,
|
||||
newEventState = if (hasUnreadBelow) NewEventState.FromOther else NewEventState.None,
|
||||
),
|
||||
timelineProtectionState = aTimelineProtectionState(),
|
||||
|
||||
+167
-11
@@ -56,6 +56,7 @@ import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
@@ -371,7 +372,7 @@ class TimelinePresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - readMarkerIndex points at the read marker virtual item`() = runTest {
|
||||
fun `present - jumpToUnread is InWindow at the read marker virtual item index`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val presenter = createTimelinePresenter(
|
||||
@@ -391,8 +392,8 @@ class TimelinePresenterTest {
|
||||
MatrixTimelineItem.Event(UniqueId("msg-newest"), anEventTimelineItem(content = aMessageContent())),
|
||||
)
|
||||
)
|
||||
consumeItemsUntilPredicate { it.readMarkerIndex != null }.last().also { state ->
|
||||
assertThat(state.readMarkerIndex).isEqualTo(3)
|
||||
consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.InWindow }.last().also { state ->
|
||||
assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.InWindow(index = 3))
|
||||
assertThat(state.displayJumpToUnread).isTrue()
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
@@ -400,7 +401,7 @@ class TimelinePresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - readMarkerIndex is null when no read marker present`() = runTest {
|
||||
fun `present - jumpToUnread is Hidden when no read marker and no fullyReadEventId`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val presenter = createTimelinePresenter(
|
||||
@@ -416,16 +417,16 @@ class TimelinePresenterTest {
|
||||
)
|
||||
)
|
||||
consumeItemsUntilPredicate {
|
||||
it.timelineItems.size == 2 && it.readMarkerIndex == null
|
||||
it.timelineItems.size == 2 && it.jumpToUnread == JumpToUnreadState.Hidden
|
||||
}.last().also { state ->
|
||||
assertThat(state.readMarkerIndex).isNull()
|
||||
assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - readMarkerIndex stays null when JumpToUnread feature flag is disabled`() = runTest {
|
||||
fun `present - jumpToUnread is Hidden when JumpToUnread feature flag is disabled`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val presenter = createTimelinePresenter(
|
||||
@@ -443,7 +444,7 @@ class TimelinePresenterTest {
|
||||
)
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 3 }.last().also { state ->
|
||||
assertThat(state.displayJumpToUnread).isFalse()
|
||||
assertThat(state.readMarkerIndex).isNull()
|
||||
assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
@@ -579,7 +580,7 @@ class TimelinePresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - readMarkerIndex is 0 when the read marker is the only item`() = runTest {
|
||||
fun `present - jumpToUnread is InWindow at index 0 when the read marker is the only item`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val presenter = createTimelinePresenter(
|
||||
@@ -591,8 +592,163 @@ class TimelinePresenterTest {
|
||||
timelineItems.emit(
|
||||
listOf(MatrixTimelineItem.Virtual(UniqueId("read-marker"), VirtualTimelineItem.ReadMarker))
|
||||
)
|
||||
consumeItemsUntilPredicate { it.readMarkerIndex != null }.last().also { state ->
|
||||
assertThat(state.readMarkerIndex).isEqualTo(0)
|
||||
consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.InWindow }.last().also { state ->
|
||||
assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.InWindow(index = 0))
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - jumpToUnread is OutOfWindow when fullyReadEventId is set but not in the loaded window`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val fullyReadEventId = EventId("\$older-than-loaded-window")
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = timeline,
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(fullyReadEventId = fullyReadEventId),
|
||||
),
|
||||
)
|
||||
val presenter = createTimelinePresenter(
|
||||
timeline = timeline,
|
||||
room = room,
|
||||
featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)),
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
// Loaded items don't include the fullyReadEventId and the SDK didn't materialise a ReadMarker.
|
||||
timelineItems.emit(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(eventId = AN_EVENT_ID, content = aMessageContent())),
|
||||
MatrixTimelineItem.Event(UniqueId("2"), anEventTimelineItem(eventId = AN_EVENT_ID_2, content = aMessageContent())),
|
||||
)
|
||||
)
|
||||
consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.OutOfWindow }.last().also { state ->
|
||||
assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.OutOfWindow(eventId = fullyReadEventId))
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - jumpToUnread is Hidden when fullyReadEventId IS in the loaded window`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = timeline,
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
// The user is caught up: the marker event is loaded, but the SDK didn't insert a
|
||||
// virtual ReadMarker because there are no items newer than it.
|
||||
initialRoomInfo = aRoomInfo(fullyReadEventId = AN_EVENT_ID),
|
||||
),
|
||||
)
|
||||
val presenter = createTimelinePresenter(
|
||||
timeline = timeline,
|
||||
room = room,
|
||||
featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)),
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
// A loaded item has eventId == AN_EVENT_ID (default of anEventTimelineItem).
|
||||
timelineItems.emit(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent())),
|
||||
)
|
||||
)
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 1 }.last().also { state ->
|
||||
assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - jumpToUnread is Hidden when fullyReadEventId is set but the timeline is empty`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = timeline,
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(fullyReadEventId = AN_EVENT_ID),
|
||||
),
|
||||
)
|
||||
val presenter = createTimelinePresenter(
|
||||
timeline = timeline,
|
||||
room = room,
|
||||
featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
// Without any timeline items, the FAB must stay hidden — the user is mid-load.
|
||||
assertThat(initialState.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden)
|
||||
advanceUntilIdle()
|
||||
val drained = consumeItemsUntilTimeout()
|
||||
assertThat(drained.any { it.jumpToUnread != JumpToUnreadState.Hidden }).isFalse()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - jumpToUnread is Hidden when fullyReadEventId is set but the feature flag is off`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = timeline,
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(fullyReadEventId = AN_EVENT_ID),
|
||||
),
|
||||
)
|
||||
val presenter = createTimelinePresenter(
|
||||
timeline = timeline,
|
||||
room = room,
|
||||
featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to false)),
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
timelineItems.emit(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent())),
|
||||
)
|
||||
)
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 1 }.last().also { state ->
|
||||
assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - jumpToUnread prefers InWindow when both a virtual marker and fullyReadEventId are present`() = runTest {
|
||||
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = timeline,
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(fullyReadEventId = AN_EVENT_ID),
|
||||
),
|
||||
)
|
||||
val presenter = createTimelinePresenter(
|
||||
timeline = timeline,
|
||||
room = room,
|
||||
featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)),
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
timelineItems.emit(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(UniqueId("msg-old"), anEventTimelineItem(content = aMessageContent())),
|
||||
MatrixTimelineItem.Virtual(UniqueId("read-marker"), VirtualTimelineItem.ReadMarker),
|
||||
MatrixTimelineItem.Event(UniqueId("msg-newest"), anEventTimelineItem(content = aMessageContent())),
|
||||
)
|
||||
)
|
||||
consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.InWindow }.last().also { state ->
|
||||
assertThat(state.jumpToUnread).isInstanceOf(JumpToUnreadState.InWindow::class.java)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user