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
An open channel marks arriving messages read with no focus or visibility gate
Problem
While a channel is the active channel, read state advances for every new top-level message as it arrives, not only when the channel is opened. Nothing in that path checks whether the window is focused or even visible.
Desktop:
ChannelScreen.tsx:213-229 recomputes activeReadAt from the live message list on every render, then passes it to useChannelOpenReadState.
useChannelOpenReadState.ts:27-42 runs a useEffect keyed on [activeChannelId, activeReadAt, ...] that calls markChannelRead(activeChannelId, activeReadAt, { topLevelOnly: true }). Because activeReadAt advances whenever a new top-level message streams in, the marker advances too.
There is no document.hasFocus(), visibilityState, blur, or Tauri window-focus check anywhere in that path. The only visibilitychange listener in readStateManager.ts:304 (handler at 967-971) flushes a pending localStorage write when the document becomes hidden — it does not gate the marker advance or the publish.
Mobile behaves the same way:
channel_detail_page.dart:207-230 derives readTimestamp from the newest top-level message; channel_detail_page.dart:557-569 marks read whenever it advances.
deferred_read_state_update.dart only defers to the next frame and checks context.mounted.
The only lifecycle handling (read_state_provider.dart:122-125) flushes on paused/detached/hidden. It is a persistence flush, not a gate.
Why this matters
A Buzz window left on a channel — backgrounded, minimised, on another desktop, or with the machine locked — marks messages read that nobody has seen. Because NIP-RS markers are monotonic and sync across devices, one parked desktop also clears the unread state on the user's phone.
There is no way to recover from it:
No setting. Neither settings UI has any read/unread preference. NotificationSettingsCard.tsx covers desktop alerts, "notify while viewing", sound and the home badge — nothing controls read marking.
"Mark unread" does not help. It is a local, per-device flag (forcedUnreadStore.ts on desktop via localStorage, an in-memory map in read_state_provider.dart:68 on mobile) that never reaches the relay. forcedUnreadStore.ts says so directly: "NOT synced to the relay — NIP-RS markers are monotonic and cannot represent a retrograde 'unread' state."
The impact is largest for agent-centric setups, where a channel is the primary surface for talking to an agent. An agent replies while the user is away with the app open, the reply is marked read, and every downstream unread signal — including anything built on NIP-RS — goes silent. The user has no indication anything arrived.
Interestingly, docs/formal/nip-rs-unread/ models a manual-unread override layer (ov_s: / ov_c: / ov_b: keys), but no client implements it; the clients only carry the local flag above. So the protocol anticipates retrograde unread while the implementation cannot express it.
Steps to reproduce
On desktop, open a channel and leave it as the active channel.
Switch to another application, or lock the screen. Leave Buzz running.
Have another member or an agent post a top-level message in that channel.
Observe a new kind:30078 read-state event whose context timestamp has advanced to the new message's created_at, within a few seconds of arrival.
The message is now read on every device. A phone signed into the same account shows no unread badge.
Expected behavior
Any of these would resolve it:
Gate the passive advance on the window actually being focused or visible on desktop, and on the app being foregrounded on mobile. This is what Slack, Discord and iMessage do.
Option 1 alone would cover the reported failure while leaving current behavior intact for a user who is genuinely at the screen.
Related
Only mark Inbox items read on explicit action #3683 — Only mark Inbox items read on explicit action. Establishes the same principle for the Inbox ("Slack-style inbox triage model"), but does not cover the channel view and does not discuss focus.
Observed on a self-hosted relay with the desktop app open on the channel and the
window not in the foreground:
message created_at = 2026-09-07 22:58:33Z (kind:9, from another member)
readstate created_at = 2026-09-07 22:58:34Z (kind:30078, d=read-state:<slot>)
The read marker advanced one second after the message arrived, without any
interaction. Decrypting the kind:30078 payload shows the channel's context
timestamp moved to the new message's created_at.
An open channel marks arriving messages read with no focus or visibility gate
Problem
While a channel is the active channel, read state advances for every new top-level message as it arrives, not only when the channel is opened. Nothing in that path checks whether the window is focused or even visible.
Desktop:
ChannelScreen.tsx:213-229recomputesactiveReadAtfrom the live message list on every render, then passes it touseChannelOpenReadState.useChannelOpenReadState.ts:27-42runs auseEffectkeyed on[activeChannelId, activeReadAt, ...]that callsmarkChannelRead(activeChannelId, activeReadAt, { topLevelOnly: true }). BecauseactiveReadAtadvances whenever a new top-level message streams in, the marker advances too.document.hasFocus(),visibilityState,blur, or Tauri window-focus check anywhere in that path. The onlyvisibilitychangelistener inreadStateManager.ts:304(handler at967-971) flushes a pending localStorage write when the document becomes hidden — it does not gate the marker advance or the publish.Mobile behaves the same way:
channel_detail_page.dart:207-230derivesreadTimestampfrom the newest top-level message;channel_detail_page.dart:557-569marks read whenever it advances.deferred_read_state_update.dartonly defers to the next frame and checkscontext.mounted.read_state_provider.dart:122-125) flushes onpaused/detached/hidden. It is a persistence flush, not a gate.Why this matters
A Buzz window left on a channel — backgrounded, minimised, on another desktop, or with the machine locked — marks messages read that nobody has seen. Because NIP-RS markers are monotonic and sync across devices, one parked desktop also clears the unread state on the user's phone.
There is no way to recover from it:
NotificationSettingsCard.tsxcovers desktop alerts, "notify while viewing", sound and the home badge — nothing controls read marking.forcedUnreadStore.tson desktop via localStorage, an in-memory map inread_state_provider.dart:68on mobile) that never reaches the relay.forcedUnreadStore.tssays so directly: "NOT synced to the relay — NIP-RS markers are monotonic and cannot represent a retrograde 'unread' state."The impact is largest for agent-centric setups, where a channel is the primary surface for talking to an agent. An agent replies while the user is away with the app open, the reply is marked read, and every downstream unread signal — including anything built on NIP-RS — goes silent. The user has no indication anything arrived.
Interestingly,
docs/formal/nip-rs-unread/models a manual-unread override layer (ov_s:/ov_c:/ov_b:keys), but no client implements it; the clients only carry the local flag above. So the protocol anticipates retrograde unread while the implementation cannot express it.Steps to reproduce
kind:30078read-state event whose context timestamp has advanced to the new message'screated_at, within a few seconds of arrival.Expected behavior
Any of these would resolve it:
Option 1 alone would cover the reported failure while leaving current behavior intact for a user who is genuinely at the screen.
Related
Logs / additional context
Observed on a self-hosted relay with the desktop app open on the channel and the
window not in the foreground:
The read marker advanced one second after the message arrived, without any
interaction. Decrypting the
kind:30078payload shows the channel's contexttimestamp moved to the new message's
created_at.Version and platform
desktop-v0.5.22deploy/compose/