Skip to content

fix(unread): read timeline mentions when catching up at the bottom - #1679

Merged
morgmart merged 2 commits into
mainfrom
morganm/unread-mention-catchup
Oct 27, 2026
Merged

morgmart merged 2 commits into
mainfrom
morganm/unread-mention-catchup

Conversation

@morgmart

@morgmart morgmart commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A Solo chat could stay bold even though you'd read everything in it. Reaching the bottom of a chat cleared normal messages, but a message that mentioned you only counted as read once the whole message had been fully on screen for 300 ms. A mention taller than the viewport, like a message with a portrait video plus text, could never meet that, so the chat stayed bold for good. We confirmed this from a real read-state journal: in the affected chat, every message was read except one tall mention.

Change

When you reach the live bottom of a channel or Solo chat, catch-up now also reads the mentions and "also send to channel" broadcast replies that the timeline holds through the bottom row. This matches Slack and Discord, where viewing the bottom of a channel clears its mentions.

It does this by writing a per-message msg: mark for each one. The activity: timestamp mark stays ordinary-only. Extending that timestamp to mentions would also have read attention delivered later with an older timestamp, such as a retried send or a fast clock, so you'd never get notified about it.

  • Messages marked unread keep their intent.
  • Each catch-up writes at most the newest 128 marks, as one observation does, so a large backlog can't crowd older marks out of the synced budget. A later catch-up marks the rest.
  • Mobile and older desktop clients already honor msg: marks, so a mention read this way is read there too.
  • Thread replies still need to be read in their thread. DMs are still read whole.

Side effects

Under the Slack model, these are all intended. After you catch up on a chat, its mentions stop counting as unread:

  • The sidebar attention dot and Solo bold clear.
  • The dock badge clears.
  • Inbox mention rows stay listed but leave the Unread filter.

While you stay focused at the live bottom, an older-stamped mention that lands above the bottom row is read on the next 300 ms dwell, the same as ordinary rows. Once you leave, nothing reads it.

Tests

  • Catch-up reads the mention and a real broadcast reply through their own marks. activity: stays ordinary-only. An older-timestamp mention delivered after catch-up stays unread and still counts as attention.
  • A mention marked unread stays unread through catch-up.
  • With 130 mentions, a catch-up marks the newest 128. A second catch-up, where activity: already covers the cut, marks the rest.
  • These fail against main's unread.ts and pass with this change. src/features/relay passes locally.

Screenshots

Not included. The only visible change is that the chat title stops being bold after you reach the bottom, which is a timing and state behavior, not a layout change, so a still image can't show it.

🤖 Generated with Claude Code

@morgmart
morgmart requested a review from a team as a code owner October 27, 2026 01:12

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 One unread-state regression needs correction.

Comment thread src/features/relay/unread.ts Resolved

@baxen baxen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 LGTM. Catch-up only reads mentions and broadcasts once the channel type is known and isn't a DM, and per-message mention marks are kept for older clients. Known tradeoff accepted: a mention that arrives late, with a timestamp at or before the catch-up mark, is read silently.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Carl, an automated reviewer, commenting via Wes's GitHub account.

Not ready to merge at 5ca9888d. Recommended blocker, detailed in Kalvin's thread at unread.ts:583: the timestamp-based activity: mark now silently reads mentions and broadcasts that arrive after catch-up with older timestamps. That covers late delivery, ±900 s clock skew, and the reply-inflated cut. Their notifications are dropped. It is also why CI required is red (WebKit notifications.spec.mjs:357).

Validation gaps

  • No journey covers the reported symptom end to end: tall mention, bottom dwell, durable read, reload. The changed tests call catchUp directly.
  • The whole-row visibility rule (use-reading.ts:103-113) is unchanged, so a tall mention above the bottom still can't be read individually. That was already true before this PR; it just means the fix is a policy change, not a visibility fix.

Merge criteria

  • Mentions and broadcasts delivered after catch-up stay unread and still notify, with a regression test. CI required green.
  • PR text: support or drop the Slack/Discord parity claim. Note the notify-while-viewing exception to notification cancellation (notifications/messages.ts:167). Also note that broadcast replies in the viewer's own threads are now read by channel catch-up and leave thread activity, which contradicts "Thread replies still need to be read in their thread."

Product call for maintainers (not a blocker): one bottom dwell now clears every mention in the channel, including history never loaded, and removes them from Inbox Unread.

Comment thread src/features/relay/unread.ts Resolved
Reaching the live bottom of a channel or Solo chat now also reads the
retained top-level mentions and broadcast replies through the bottom
row, as Slack and Discord do. Before, each mention had to sit fully on
screen for a dwell, so a mention taller than the viewport could never be
read and kept its chat bold.

Catch-up writes a `msg:` mark for each of them rather than extending the
`activity:` timestamp, which would also read attention delivered later
with an older timestamp (a retried send, a fast clock). Messages marked
unread keep their intent, and mobile and older desktop clients honor the
marks. Thread replies and DMs are unchanged.

Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: morgmart <[email protected]>
@morgmart
morgmart force-pushed the morganm/unread-mention-catchup branch from 5ca9888 to 4ad39ea Compare October 27, 2026 02:02
Mark at most the newest 128 retained mentions and broadcasts per
catch-up, as one observation does, so a large backlog cannot crowd older
marks out of the synced read-state budget. A later catch-up marks the
rest, even when `activity:` already covers its cut.

Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: morgmart <[email protected]>
@morgmart
morgmart merged commit 5159f64 into main Oct 27, 2026
37 checks passed
@morgmart
morgmart deleted the morganm/unread-mention-catchup branch October 27, 2026 02:22
blakemcanally added a commit to block/buzz that referenced this pull request Nov 9, 2026
## Problem

Bug report (mobile 0.28.0 (46)): opening a mention from the Inbox in an
archived Solo chat leaves the Inbox row unread.

The video shows the cause. Each unread row is a long agent answer. On a
phone, the answer is taller than the screen, or it lands partly cut off
after the deep-link jump. Mobile automatic reading writes a `msg:` mark
only for a row that is fully visible for the 300 ms dwell. A mention row
is high priority, so `activity:<channel>` does not read it. The mention
can stay unread until the reader marks it read by hand.

The archived state is not the cause. A widget test shows that an
archived Solo chat writes read marks for short messages. Archived chats
make the problem easy to see, because there is no composer and the
reader only reads.

## Fix

A message counts as read only when it actually showed, for the 300 ms
dwell. A row counts when any of these holds:

- **It fits:** the whole row is inside the reading area (unchanged).
- **It fills the screen:** the row is taller than the reading area and
covers all of it. This applies to the channel timeline and the thread
view.
- **A link opened it:** the message a deep link or Inbox tap opened
counts once any part of it shows, because the jump places it at 35% and
a long one lands cut off.
- **It is the newest message at the live bottom:** its end is showing,
in a channel, a Solo chat (replies inline) or a thread. This covers a
long final answer that only ever shows its end.

Reaching the bottom does **not** read mentions or broadcasts above the
newest message that never came on screen. Desktop does that sweep
(block/buzz-app#1679). Mobile deliberately does not. Ordinary
`activity:` / `thread-activity:` catch-up is unchanged, and messages the
reader marked unread stay unread.

## Tests

- `reading_marks_test.dart`: the bottom does not read mentions above it;
the newest message is read at the live bottom (and respects manual
unread); a flat Solo timeline reads only its newest inline reply; a tall
thread tail reply is read at its end; DM catch-up does not mark
mentions; `isReadableRow` cases, including linked rows.
- `channel_detail_page_test.dart`: an archived Solo chat and a Solo
inline reply each read a tall newest mention at the bottom; a tall row
away from the bottom is read once it fills the screen; tall linked
messages (channel and thread) are read when the jump leaves them cut
off; in a thread, the tall last reply is read at the end and an earlier
tall reply is read once it fills the screen. These fail on `main`.
- `just mobile-check` and the full mobile suite pass.

## Human test steps

1. Get a long agent reply that mentions you in a Solo chat, then archive
the chat.
2. Open the row from the Inbox and hold still for about one second.
3. Go back. The row should now show as read, with no dot.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Blake McAnally <[email protected]>
Signed-off-by: morgmart <[email protected]>
Co-authored-by: BlakeBot <85bbd70443139fd5c5762e34c2e214240939eb243c37f98380f69fed3b3bb4a6@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: morgmart <[email protected]>
Co-authored-by: Morgarita <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@buzz.block.builderlab.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants