Skip to content

fix(llc): handle per-channel notification.mark_read events again - #2890

Merged
xsahil03x merged 2 commits into
masterfrom
fix/notification-mark-read-unread-count
Aug 14, 2026
Merged

fix(llc): handle per-channel notification.mark_read events again#2890
xsahil03x merged 2 commits into
masterfrom
fix/notification-mark-read-unread-count

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Aug 14, 2026

Copy link
Copy Markdown
Member

Linear: FLU-693
Github Issue: #2886

CLA

  • I have signed the Stream CLA (required).
  • The code changes follow best practices
  • Code changes are tested

Description of the pull request

ChannelClientState._listenReadEvents subscribed to both message.read and notification.mark_read until the delivery-receipts refactor (#2429, 3b5e410bb) split them into separate listeners and did not carry notification.mark_read over. The changelog documents the rest of that refactor but not this removal, so it looks unintentional.

That distinction is load-bearing:

  • message.read is a channel event — delivered to watchers only.
  • notification.mark_read is delivered on the reading user's own connection — so it reached the channel whether watched or not.

Channel.markRead() writes no local state itself, and channels are easily non-watched (watch is silently downgraded when there is no connection id). So from 9.20.0 on, marking a non-watched channel as read succeeded server-side while unreadCount stayed stale forever — the unread pill and divider survived the dismiss X, markReadWhenAtTheBottom, and every reopen, with no user action able to clear them.

The fix re-adds the event type to the existing listener:

_channel.on(EventType.messageRead, EventType.notificationMarkRead).listen(

Not a literal revert. The handler has improved since 9.19.0 — it now skips thread-scoped reads and preserves lastDeliveredAt / lastDeliveredMessageId — and the merged listener keeps both. Nothing from #2429 is undone.

Verification against the backend event definition

Rather than infer the payload, I checked lib/chat/event/notification_mark_read.go. The two events are the same shape for every field the handler reads:

Handler reads message.read notification.mark_read
event.thread thread,omitempty thread,omitempty
event.user yes yes — non-nil at both emit sites
event.createdAt yes yes
event.lastReadMessageId yes yes
per-channel unread count absent absent

Which settles three things:

  • unreadMessages: 0 is correct, not a compromise. unread_messages exists on exactly one event in the backend — notification.mark_unread. On notification.mark_read, unread_count / total_unread_count are account-wide totals, and unread_count is deprecated. Neither event can report this channel's count, so 0 is the only available value.
  • No last_read_at, so lastRead: event.createdAt is right.
  • The thread guard is load-bearing. NewNotificationMarkThreadReadEvent sets Thread, so thread mark-reads do arrive on this type and would otherwise clobber the channel-level Read.

Also confirmed: read_state.go returns early when !channel.Config.ReadEvents, so no event is emitted for read-events-disabled channel types — channels using local unread counts are unaffected. And markAllRead() posts {}, which routes to MarkAllRead → an event with no cid, exactly what the client-level handler's if (event.cid != null) return; expects. The channel-level / client-level split is correct.

Testing

Four tests in channel_test.dart. I verified they actually catch the regression by reverting the one-line fix and re-running — three go red:

Test Without the fix
resets unread count on a non-watched channel fails: Expected: <0> Actual: <10>
preserves delivery info fails: Expected: <0> Actual: <10>
reconciles delivery for the current user fails: No matching calls
ignores thread-scoped mark-reads passes (negative test, guards the guard)

Full stream_chat suite: 1624 passing. dart analyze --fatal-infos and dart format clean.

Out of scope

  • Client-level counters are not broken on master. The issue reports totalUnreadCount / unreadChannels going stale too, and the reporter shipped a client-level listener for it. That part isn't needed — ClientState.subscribeToEvents applies both from any event carrying them, and set currentUser pumps them through _computeUnreadCounts.
  • Persistence package causes double unread count to appear in scroll to bottom button and no unread count on the back button when opened from message notification #2202 (reconnect sync() replay re-incrementing unreadCount via the non-idempotent addNewMessage) is real and related — through 9.19.0 this reset washed that drift away on the next mark-read — but it's a separate bug.
  • Test-coverage gap worth its own PR: 10 other event subscriptions in channel.dart have zero test references anywhere in the suite, so any could be silently dropped the same way with CI staying green — channel.truncated, notification.channel_truncated, channel.updated, member.added, member.removed, user.banned, user.unbanned, reaction.updated, poll.closed, poll.updated. Tracked in FLU-693's notes.

Screenshots / Videos

Not applicable — no visual change beyond the unread pill and divider now clearing, which is the bug itself.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed unread counts and read-state updates when marking a channel as read while not actively viewing it.
    • Preserved delivery information and correctly handled read events, including current-user updates.
    • Ignored thread-specific read notifications when updating channel-level state.
  • Tests

    • Added coverage for unread counts, read states, delivery details, and thread-scoped notifications.

`ChannelClientState._listenReadEvents` subscribed to both `message.read`
and `notification.mark_read` until the delivery-receipts refactor (#2429)
split them and dropped the latter.

`message.read` only reaches watchers, so from 9.20.0 on `markRead()` on a
non-watched channel left `unreadCount` permanently stale — the unread pill
and divider could not be cleared by any user action.

Re-adds the event type to the existing listener, which since 9.19.0 also
guards against thread-scoped reads and preserves delivery fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3923f914-24ca-4eea-8c42-850a86aa58cb

📥 Commits

Reviewing files that changed from the base of the PR and between cb43fc4 and a9b0482.

📒 Files selected for processing (3)
  • packages/stream_chat/CHANGELOG.md
  • packages/stream_chat/lib/src/client/channel.dart
  • packages/stream_chat/test/src/client/channel_test.dart

📝 Walkthrough

Walkthrough

The channel read-event listener now handles notification.mark_read events. Tests cover channel and thread read state, unread counts, delivery metadata, and current-user delivery reconciliation. The changelog documents the fix.

Changes

Notification mark-read handling

Layer / File(s) Summary
Read-event handling
packages/stream_chat/lib/src/client/channel.dart, packages/stream_chat/CHANGELOG.md
The listener processes notification.mark_read alongside message.read and applies the existing read-state updates and delivery reconciliation.
Read-state validation
packages/stream_chat/test/src/client/channel_test.dart
Tests cover unread-count resets, delivery metadata preservation, thread-scoped events, and current-user delivery reconciliation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to a9b04

This is a localized fix that restores per-channel read notifications and is covered by targeted tests; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: testableapple

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the restored handling of per-channel notification.mark_read events.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/notification-mark-read-unread-count

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.96%. Comparing base (cb43fc4) to head (ecbe969).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #2890   +/-   ##
=======================================
  Coverage   73.96%   73.96%           
=======================================
  Files         435      435           
  Lines       28149    28149           
=======================================
  Hits        20821    20821           
  Misses       7328     7328           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@xsahil03x
xsahil03x merged commit 3c6ad32 into master Aug 14, 2026
29 checks passed
@xsahil03x
xsahil03x deleted the fix/notification-mark-read-unread-count branch August 14, 2026 11:44
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.

2 participants