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
- Receive messages in a channel while the app is closed.
- 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).
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.
Describe the bug
Since 9.20.0,
Channel.markRead()on a non-watched channel leaves the local unread state permanently stale:ChannelClientState.unreadCountis never reset, so theUnreadIndicatorButtonpill andUnreadMessagesSeparatordivider 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 currentmaster.Root cause (verified against published sources)
ChannelClientState._listenReadEventssubscribed to bothmessage.readandnotification.mark_read(9.19.0channel.dart:3101), resetting the unread count from the server-supplied value.notification.mark_readis 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,unreadMessages10 → 0).markReadthrowing withoutread_events) but not this removal, which suggests it was unintentional. From 9.20.0 on,_listenReadEventshandles onlymessage.read(10.2.0channel.dart:3356) — a channel event that reaches watchers only.notification.mark_readbails on cid-scoped events (10.2.0client.dart:2670,if (event.cid != null) return;), so a per-channelnotification.mark_readis now handled nowhere — orphaning the client-leveltotalUnreadCount/unreadChannelstoo.notificationMarkReadhas zero hits in 10.2.0's channel tests.Channel.markRead()itself writes no local state (10.2.0channel.dart:1855-1866), and channels are easily non-watched:watchis silently downgraded when there is no connection id (client.dart:741-744), andStreamChannel._maybeInitChannelonly re-watches whenchannel.state == null.isLocalUnreadCountEnabled/markReadLocallywork 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
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-incrementsunreadCountvia the non-idempotentaddNewMessage(auto-closed as stale; still reproducible on 10.2.0). Through 9.19.0 thenotification.mark_readreset 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 racingmessage.readechoes via alastReadsnapshot), plus our own client-levelnotification.mark_readlistener applyingevent.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.