Skip to content

Per-channel notification.mark_read handling was silently dropped in 9.20.0 — unreadCount never resets on non-watched channels #2886

Description

@lukasgrenstad

Describe the bug

Since 9.20.0, Channel.markRead() on a non-watched channel leaves the local unread state permanently stale: ChannelClientState.unreadCount is never reset, so the UnreadIndicatorButton pill and UnreadMessagesSeparator divider survive the dismiss X, markReadWhenAtTheBottom, and every reopen of the channel. The client-level counters (totalUnreadCount / unreadChannels) go stale the same way. Still present in 10.2.0 and on current master.

Root cause (verified against published sources)

  • Through 9.19.0, ChannelClientState._listenReadEvents subscribed to both message.read and notification.mark_read (9.19.0 channel.dart:3101), resetting the unread count from the server-supplied value. notification.mark_read is delivered on the reading user's own connection, so it reached the channel whether watched or not — and the 9.19.0 tests asserted exactly that (channel_test.dart:4364-4410, unreadMessages 10 → 0).
  • 9.20.0's delivery-receipts refactor removed that subscription; the changelog documents the rest of the refactor (capability checks, markRead throwing without read_events) but not this removal, which suggests it was unintentional. From 9.20.0 on, _listenReadEvents handles only message.read (10.2.0 channel.dart:3356) — a channel event that reaches watchers only.
  • The only remaining client-level consumer of notification.mark_read bails on cid-scoped events (10.2.0 client.dart:2670, if (event.cid != null) return;), so a per-channel notification.mark_read is now handled nowhere — orphaning the client-level totalUnreadCount / unreadChannels too. notificationMarkRead has zero hits in 10.2.0's channel tests.
  • Channel.markRead() itself writes no local state (10.2.0 channel.dart:1855-1866), and channels are easily non-watched: watch is silently downgraded when there is no connection id (client.dart:741-744), and StreamChannel._maybeInitChannel only re-watches when channel.state == null.
  • The unreleased isLocalUnreadCountEnabled / markReadLocally work does not cover this: it targets read-events-disabled channel types by design ("Channels that support read receipts are unaffected and keep relying on server-driven unread counts").

Steps to reproduce

  1. Receive messages in a channel while the app is closed.
  2. Cold-start the app and open that channel before/without it becoming watched (e.g. from a push notification, or with the WS handshake still pending).
  3. markRead() succeeds (the server clears its side), but the unread pill/divider and the unread counts never clear locally — and cannot be cleared by any user action.

Related

#2202 — the reconnect sync() replay re-increments unreadCount via the non-idempotent addNewMessage (auto-closed as stale; still reproducible on 10.2.0). Through 9.19.0 the notification.mark_read reset washed that drift away on the next mark-read; with it gone, the inflation is permanent as well.

Workaround we shipped app-side

Mirror the reset after a successful markRead() (channel.state.unreadCount = 0, guarded against racing message.read echoes via a lastRead snapshot), plus our own client-level notification.mark_read listener applying event.totalUnreadCount / event.unreadChannels.

Environment

stream_chat / stream_chat_flutter / stream_chat_persistence 10.2.0 (first affected: 9.20.0), Flutter 3.44.0, iOS + Android.

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