You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consider adding a button to jump to the last message before you opened River. Otherwise I have to figure out where to resume reading.
River already tracks the data this needs. The gap is purely UI.
What exists:
RoomData.last_read_message_id (ui/src/room_data.rs:53), a per-room marker persisted to delegate storage, advanced when the user opens the room, when a message arrives while viewing, and on tab hide.
It already drives count_unread_in_room_data (ui/src/components/app/document_title.rs:172), the document-title badge, the room-list badge (ui/src/components/room_list.rs:213), and the mobile hamburger badge.
What is missing:
Switching rooms unconditionally force-scrolls to the newest message (ui/src/components/conversation.rs:1305-1318, added by Suggestions for UI improvements for mobile and touch users #402). No code path scrolls to the first unread, and there is no divider in the scrollback.
So the marker is maintained and counted, then ignored when deciding where to put the reader.
Proposal
Render an unread divider ("New messages") at the first message after last_read_message_id.
On room switch, if unread > 0, scroll to the divider instead of the bottom.
Keep the existing scroll-to-latest button (conversation.rs:2238-2268) for returning to the present.
This is the Slack / Discord / Element pattern, and it is a concrete implementation of the first bullet of #70.
Performance constraints
PR #458 just fixed mobile jank caused by an O(history) use_memo re-running over the entire message list on every ROOMS mutation. Do not reintroduce that shape.
Freeze the divider boundary at room-open, in a signal. Do not derive it from a memo over ROOMS. Two independent reasons: recomputing on every mutation re-does O(history) work on every sync tick, reaction and inbound message; and a live boundary would slide away as you read, when the divider should stay put (Slack and Discord both freeze it).
The boundary scan is O(<=100), since max_recent_messages defaults to 100 (common/src/room_state/configuration.rs:179), and it duplicates work count_unread_in_room_data already performs. Worth sharing rather than adding a second pass.
Pruned marker.apply_delta drains the oldest messages beyond max_recent_messages (common/src/room_state/message.rs:229), so the marked message can be evicted from the ring buffer. count_unread_in_room_data already falls back to treating everything as unread (document_title.rs:176-184, pinned by pruned_marker_falls_back_to_all_other_authored at 692-704). Open question: should the divider then sit at the top of the buffer, or be suppressed entirely? Suppressing is probably less confusing, since "everything here is new" is rarely true and the top of a 100-message buffer is an arbitrary place to anchor.
Zero unread. No divider, keep the current scroll-to-bottom.
Direct messages track last-seen separately and in memory only (DM_LAST_SEEN, ui/src/components/direct_messages.rs:42, seeded once per session), so DMs are out of scope here unless we want parity in the same pass.
mark_all_rooms_as_read() fires on every visible to hidden visibilitychange (document_title.rs:430-480), so on mobile an app-switch marks every room read, including rooms that were never on screen. A jump-to-unread button that points at a wiped marker is worse than no button at all, and #446 may well be the underlying cause of the original report.
#446 already asks for a product call between three options. Option 1 there ("only mark the current room read on hide") is what this feature requires. @sanity, that is the call #446 has been waiting on.
Recommend resolving #446 as part of this work rather than shipping the button on top of a marker that gets erased.
Not in scope
The second bullet of #70 (append inbound messages to the bottom regardless of timestamp) is a separate display-ordering question and is not proposed here.
Problem
User request via Matrix (2026-07-23):
River already tracks the data this needs. The gap is purely UI.
What exists:
RoomData.last_read_message_id(ui/src/room_data.rs:53), a per-room marker persisted to delegate storage, advanced when the user opens the room, when a message arrives while viewing, and on tab hide.count_unread_in_room_data(ui/src/components/app/document_title.rs:172), the document-title badge, the room-list badge (ui/src/components/room_list.rs:213), and the mobile hamburger badge.What is missing:
ui/src/components/conversation.rs:1305-1318, added by Suggestions for UI improvements for mobile and touch users #402). No code path scrolls to the first unread, and there is no divider in the scrollback.So the marker is maintained and counted, then ignored when deciding where to put the reader.
Proposal
last_read_message_id.conversation.rs:2238-2268) for returning to the present.This is the Slack / Discord / Element pattern, and it is a concrete implementation of the first bullet of #70.
Performance constraints
PR #458 just fixed mobile jank caused by an O(history)
use_memore-running over the entire message list on everyROOMSmutation. Do not reintroduce that shape.ROOMS. Two independent reasons: recomputing on every mutation re-does O(history) work on every sync tick, reaction and inbound message; and a live boundary would slide away as you read, when the divider should stay put (Slack and Discord both freeze it).max_recent_messagesdefaults to 100 (common/src/room_state/configuration.rs:179), and it duplicates workcount_unread_in_room_dataalready performs. Worth sharing rather than adding a second pass.Edge cases
apply_deltadrains the oldest messages beyondmax_recent_messages(common/src/room_state/message.rs:229), so the marked message can be evicted from the ring buffer.count_unread_in_room_dataalready falls back to treating everything as unread (document_title.rs:176-184, pinned bypruned_marker_falls_back_to_all_other_authoredat 692-704). Open question: should the divider then sit at the top of the buffer, or be suppressed entirely? Suppressing is probably less confusing, since "everything here is new" is rarely true and the top of a 100-message buffer is an arbitrary place to anchor.DM_LAST_SEEN,ui/src/components/direct_messages.rs:42, seeded once per session), so DMs are out of scope here unless we want parity in the same pass.Blocked on a decision in #446
mark_all_rooms_as_read()fires on every visible to hiddenvisibilitychange(document_title.rs:430-480), so on mobile an app-switch marks every room read, including rooms that were never on screen. A jump-to-unread button that points at a wiped marker is worse than no button at all, and #446 may well be the underlying cause of the original report.#446 already asks for a product call between three options. Option 1 there ("only mark the current room read on hide") is what this feature requires. @sanity, that is the call #446 has been waiting on.
Recommend resolving #446 as part of this work rather than shipping the button on top of a marker that gets erased.
Not in scope
The second bullet of #70 (append inbound messages to the bottom regardless of timestamp) is a separate display-ordering question and is not proposed here.
Related: #70, #446, #402, #458
[AI-assisted - Claude]