Skip to content

An open channel marks arriving messages read with no focus or visibility gate #7470

Description

@MrStuartBell

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

  1. On desktop, open a channel and leave it as the active channel.
  2. Switch to another application, or lock the screen. Leave Buzz running.
  3. Have another member or an agent post a top-level message in that channel.
  4. 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.
  5. 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:

  1. 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.
  2. Offer a setting to disable passive mark-as-read.
  3. Apply the model already proposed in Only mark Inbox items read on explicit action #3683 to the channel view: viewing previews, and read state changes on explicit action.

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:

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.

Version and platform

  • Buzz version: desktop-v0.5.22
  • OS: macOS 15 (Darwin 25.6)
  • Relay: self-hosted, deploy/compose/

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions