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()
|
||||
}
|
||||
|
||||
+1
@@ -131,6 +131,7 @@ private fun aSpaceInfo(
|
||||
numUnreadMessages = 0,
|
||||
numUnreadNotifications = 0,
|
||||
numUnreadMentions = 0,
|
||||
fullyReadEventId = null,
|
||||
heroes = persistentListOf(),
|
||||
pinnedEventIds = persistentListOf(),
|
||||
creators = persistentListOf(),
|
||||
|
||||
@@ -178,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
|
||||
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||
# only fix API breaks and may add some TODOs.
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.7"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.20"
|
||||
|
||||
# Others
|
||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||
|
||||
+5
@@ -70,6 +70,11 @@ data class RoomInfo(
|
||||
* notification settings.
|
||||
*/
|
||||
val numUnreadMentions: Long,
|
||||
/**
|
||||
* Event ID of the user's `m.fully_read` marker for this room, if any.
|
||||
* Can be set even when the event is older than the loaded timeline window.
|
||||
*/
|
||||
val fullyReadEventId: EventId?,
|
||||
val heroes: ImmutableList<MatrixUser>,
|
||||
val pinnedEventIds: ImmutableList<EventId>,
|
||||
val creators: ImmutableList<UserId>,
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||
}
|
||||
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.EventCache -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.InvalidRawKey -> AuthenticationException.Generic(message)
|
||||
}
|
||||
is OAuthException -> when (this) {
|
||||
is OAuthException.Generic -> AuthenticationException.OAuth(message)
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ class RoomInfoMapper {
|
||||
numUnreadMessages = it.numUnreadMessages.toLong(),
|
||||
numUnreadMentions = it.numUnreadMentions.toLong(),
|
||||
numUnreadNotifications = it.numUnreadNotifications.toLong(),
|
||||
fullyReadEventId = it.fullyReadEventId?.let(::EventId),
|
||||
historyVisibility = it.historyVisibility.map(),
|
||||
successorRoom = it.successorRoom?.map(),
|
||||
roomVersion = it.roomVersion,
|
||||
|
||||
+2
@@ -52,6 +52,7 @@ internal fun aRustRoomInfo(
|
||||
numUnreadMessages: ULong = 0uL,
|
||||
numUnreadNotifications: ULong = 0uL,
|
||||
numUnreadMentions: ULong = 0uL,
|
||||
fullyReadEventId: String? = null,
|
||||
pinnedEventIds: List<String> = listOf(),
|
||||
roomCreators: List<String>? = emptyList(),
|
||||
joinRule: JoinRule? = null,
|
||||
@@ -93,6 +94,7 @@ internal fun aRustRoomInfo(
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
numUnreadNotifications = numUnreadNotifications,
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
fullyReadEventId = fullyReadEventId,
|
||||
pinnedEventIds = pinnedEventIds,
|
||||
creators = roomCreators,
|
||||
joinRule = joinRule,
|
||||
|
||||
+3
-3
@@ -40,8 +40,8 @@ class FakeFfiSpaceRoomList(
|
||||
return paginationStateResult()
|
||||
}
|
||||
|
||||
override fun rooms(): List<SpaceRoom> {
|
||||
return roomsResult()
|
||||
override suspend fun rooms(): List<SpaceRoom> = simulateLongTask {
|
||||
roomsResult()
|
||||
}
|
||||
|
||||
override fun subscribeToPaginationStateUpdates(listener: SpaceRoomListPaginationStateListener): TaskHandle {
|
||||
@@ -53,7 +53,7 @@ class FakeFfiSpaceRoomList(
|
||||
spaceRoomListPaginationStateListener?.onUpdate(state)
|
||||
}
|
||||
|
||||
override fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle {
|
||||
override suspend fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle {
|
||||
spaceRoomListEntriesListener = listener
|
||||
return FakeFfiTaskHandle()
|
||||
}
|
||||
|
||||
+3
@@ -79,6 +79,7 @@ class RoomInfoMapperTest {
|
||||
numUnreadMessages = 12uL,
|
||||
numUnreadNotifications = 13uL,
|
||||
numUnreadMentions = 14uL,
|
||||
fullyReadEventId = AN_EVENT_ID.value,
|
||||
pinnedEventIds = listOf(AN_EVENT_ID.value),
|
||||
roomCreators = listOf(A_USER_ID.value),
|
||||
historyVisibility = RustRoomHistoryVisibility.Joined,
|
||||
@@ -131,6 +132,7 @@ class RoomInfoMapperTest {
|
||||
numUnreadMessages = 12L,
|
||||
numUnreadNotifications = 13L,
|
||||
numUnreadMentions = 14L,
|
||||
fullyReadEventId = AN_EVENT_ID,
|
||||
historyVisibility = RoomHistoryVisibility.Joined,
|
||||
successorRoom = null,
|
||||
roomVersion = "12",
|
||||
@@ -223,6 +225,7 @@ class RoomInfoMapperTest {
|
||||
numUnreadMessages = 12L,
|
||||
numUnreadNotifications = 13L,
|
||||
numUnreadMentions = 14L,
|
||||
fullyReadEventId = null,
|
||||
historyVisibility = RoomHistoryVisibility.Joined,
|
||||
roomVersion = "12",
|
||||
privilegedCreatorRole = true,
|
||||
|
||||
+2
@@ -66,6 +66,7 @@ fun aRoomInfo(
|
||||
numUnreadMessages: Long = 0,
|
||||
numUnreadNotifications: Long = 0,
|
||||
numUnreadMentions: Long = 0,
|
||||
fullyReadEventId: EventId? = null,
|
||||
historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined,
|
||||
roomVersion: String? = "11",
|
||||
privilegedCreatorRole: Boolean = false,
|
||||
@@ -105,6 +106,7 @@ fun aRoomInfo(
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
numUnreadNotifications = numUnreadNotifications,
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
fullyReadEventId = fullyReadEventId,
|
||||
historyVisibility = historyVisibility,
|
||||
roomVersion = roomVersion,
|
||||
privilegedCreatorRole = privilegedCreatorRole,
|
||||
|
||||
+1
@@ -115,6 +115,7 @@ fun aRoomSummary(
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
numUnreadNotifications = numUnreadNotifications,
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
fullyReadEventId = null,
|
||||
historyVisibility = historyVisibility,
|
||||
roomVersion = roomVersion,
|
||||
privilegedCreatorRole = privilegedCreatorRole,
|
||||
|
||||
Reference in New Issue
Block a user