feat(ui, core, llc, localization): unread banners - #2871
Conversation
Tracks whether the current user has an active manual mark-unread on the channel that hasn't been read past yet, mirroring the iOS SDK's ReadStateHandler.isMarkedAsUnread. Set by markUnreadLocally and by a notification.mark_unread event for the current user; cleared by markReadLocally and by a message.read event for the current user. Intended for UI-layer gating that shouldn't immediately undo a manual mark-unread — used by stream_chat_flutter's tightened mark-read gating (FLU-640). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gates the existing auto-scroll-to-first-unread positioning behind an opt-out flag on StreamChannel/StreamChannel.value, defaulting to true so existing integrations keep today's behavior unchanged. Set to false to always open a channel at the latest message instead, and let the message list surface pre-existing unread via its divider and jump-to-unread pill rather than by scrolling there automatically. Updates the sample app's channel route to demonstrate the flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
/649/650/640) Unread messages divider: anchored to the pre-existing read/unread boundary captured when the channel opens. The anchor is frozen for the whole session — it never moves or disappears, regardless of scrolling or reads — but its displayed count keeps counting up as further messages arrive during the session (mirroring WhatsApp) instead of staying fixed at the open-time total. Jump-to-unread pill (UnreadIndicatorButton): shows the frozen open-time count, gated on that boundary sitting above the viewport. Visible as soon as the count is known from the channel's Read state, even before the boundary message itself has loaded — tapping it before then falls back to loadChannelAtMessage via the boundary's lastReadMessageId. Dismisses permanently for the session on tap, the dismiss button, or scrolling past it; the button itself is now purely presentational, taking a required unreadCount instead of subscribing to read state internally. Scroll-to-bottom badge: counts only messages that arrive while scrolled away from the bottom (never seeded from the channel's unread count, unlike the divider above), and always resets to 0 once the user reaches the bottom. Mark-read gating (FLU-640): tightened to mirror iOS's shouldMarkChannelRead — besides isUpToDate and unreadCount > 0, now also requires the bottom to have been seen (now, or earlier then scrolled away), the pre-existing boundary (if any) to have been seen or scrolled past, and no active manual mark-unread (Channel.isMarkedAsUnread). That last check can't gate on the flag directly and permanently: it only clears via a successful mark-read, which is the very thing it would be gating, so it would deadlock the channel unread forever the moment it's set. Instead it latches once the viewport genuinely diverges from a snapshot taken when the mark-unread was first observed — captured eagerly on a live transition, or on the first laid-out frame as a fallback for a channel that simply mounts already marked unread. Adds StreamMessageListViewConfiguration.shouldMarkRead to override this gating entirely, and Translations.unreadMessagesSeparatorLabel (added rather than changing the existing unreadMessagesSeparatorText, to avoid breaking existing overrides) so the default separator can show a count. Also defaults MockChannelState.isMarkedAsUnread to false, since _handleItemPositionsChanged now reads it on every scroll tick and existing test files that construct the mock without stubbing it would otherwise crash. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the new count-aware label (Translations.unreadMessagesSeparatorLabel, introduced in stream_chat_flutter) across all 11 supported locales, plus the add_new_lang.dart example template and test coverage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughWalkthroughThe PR adds manual unread state tracking, count-based unread indicators and separators, configurable automatic mark-read gating, revised unread divider and scroll-to-bottom behavior, and an ChangesUnread state and message list flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant MessageStream
participant StreamMessageListView
participant ChannelClientState
MessageStream->>StreamMessageListView: Deliver messages and read-state events
StreamMessageListView->>StreamMessageListView: Track unread anchor and viewport state
StreamMessageListView->>ChannelClientState: Evaluate mark-read conditions
ChannelClientState-->>StreamMessageListView: Apply current-user read state
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
packages/stream_chat_flutter/test/src/message_list_view/mark_read_test.dart (1)
328-370: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert on the
detailspassed toshouldMarkRead.Both override tests return a constant and ignore
details. They prove the override is consulted but not that it receives correct inputs. In the second test the default gating blocks the read, sodetails.hasSeenFirstUnreadMessagemust befalseanddetails.unreadCountmust be5. Capturing and asserting those fields protects theStreamMarkReadDetailscontract.Consider also adding a case where the channel has an active manual mark-unread and a new message arrives. That path is currently uncovered and is where the viewport-divergence signal is weakest.
💚 Sketch
late StreamMarkReadDetails captured; await pumpMessageList( tester, // ... shouldMarkRead: (details) { captured = details; return true; }, ); expect(captured.unreadCount, 5); expect(captured.hasSeenFirstUnreadMessage, isFalse); expect(captured.isMarkedAsUnread, isFalse);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stream_chat_flutter/test/src/message_list_view/mark_read_test.dart` around lines 328 - 370, Update the two shouldMarkRead override tests to capture the StreamMarkReadDetails argument and assert its contract instead of only returning a constant: in the override-blocking test validate the relevant unread/visibility values, and in the allowing test assert unreadCount is 5, hasSeenFirstUnreadMessage is false, and isMarkedAsUnread is false. Also add coverage for an active manual mark-unread followed by a new message, asserting the details passed through that path.packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_view_configuration.dart (1)
223-224: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDocument the closure-identity caveat for
shouldMarkRead.Dart compares closures by identity. A predicate written inline in
buildcreates a new closure on every build, so two configurations that are otherwise identical compare unequal. Hosts that rely on configuration equality should hoist the predicate into a field or a static function.Adding one sentence to the
shouldMarkReaddoc comment prevents that surprise.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_view_configuration.dart` around lines 223 - 224, Update the shouldMarkRead documentation near the configuration equality logic to add one sentence explaining that inline predicates create new closure identities and can make otherwise identical configurations unequal. Advise hosts relying on configuration equality to hoist the predicate into a field or static function; do not change the equality implementation.packages/stream_chat_flutter_core/lib/src/stream_channel.dart (1)
873-905: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNote that
openAtFirstUnreadis read only during initialization.
didUpdateWidgetre-initializes the channel only whenchannel.cidorinitialMessageIdchanges. A host that flipsopenAtFirstUnreadafter mount therefore sees no repositioning. That is a reasonable choice, because repositioning on a flag change would move the user's viewport unexpectedly.Adding one sentence to the property doc ("read once during initialization") removes the ambiguity for host developers.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stream_chat_flutter_core/lib/src/stream_channel.dart` around lines 873 - 905, Update the documentation for the openAtFirstUnread property to state that it is read only during channel initialization and changes after mount do not reposition the current viewport. Keep the existing initialization and didUpdateWidget behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/stream_chat_flutter/lib/src/message_list_view/mark_read_details.dart`:
- Around line 1-6: Update the mark-read type imports to use the package barrel
instead of direct src paths: in stream_message_list_view_configuration.dart,
import StreamMarkReadDetails and StreamShouldMarkReadPredicate from
package:stream_chat_flutter/stream_chat_flutter.dart, and in
mark_read_details.dart either route the StreamMessageListView and
StreamMessageListViewConfiguration.shouldMarkRead dartdoc references through the
barrel or change them to plain text so the docs no longer depend on src-only
symbols.
In
`@packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart`:
- Around line 1105-1113: Move the `_hasSeenFirstUnread.value = true` assignment
in `_onUnreadPillJumpTap` to after the awaited `_scrollToMessage` call, and only
set it when that jump reports success. Preserve the early return for a missing
anchor and keep the unread pill visible when `_scrollToMessage` cannot scroll
because the target or controller is unavailable.
- Around line 1477-1482: Update the isScrolledPast calculation in the
unread-boundary logic to account for widget.config.reverse: retain the current
comparison for reversed lists and use the opposite index-direction comparison
when reverse is false. Preserve the existing isAnchorVisible and return behavior
so _hasSeenFirstUnread only advances after the user actually scrolls past the
anchor in either layout.
- Around line 1443-1454: The _checkMarkUnreadViewportDivergence method currently
compares full ItemPosition values, allowing fractional edge changes to falsely
signal viewport divergence. Update the comparison to use only visible item
indices, or otherwise base divergence on actual scroll activity, while
preserving the initial snapshot and existing _markUnreadViewportDiverged guard
behavior.
- Around line 586-589: Add `_showScrollToBottom.dispose()` to the state teardown
alongside the other notifier disposals, ensuring the ValueNotifier created for
the scroll-to-bottom widget is released when the widget unmounts.
---
Nitpick comments:
In `@packages/stream_chat_flutter_core/lib/src/stream_channel.dart`:
- Around line 873-905: Update the documentation for the openAtFirstUnread
property to state that it is read only during channel initialization and changes
after mount do not reposition the current viewport. Keep the existing
initialization and didUpdateWidget behavior unchanged.
In
`@packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_view_configuration.dart`:
- Around line 223-224: Update the shouldMarkRead documentation near the
configuration equality logic to add one sentence explaining that inline
predicates create new closure identities and can make otherwise identical
configurations unequal. Advise hosts relying on configuration equality to hoist
the predicate into a field or static function; do not change the equality
implementation.
In `@packages/stream_chat_flutter/test/src/message_list_view/mark_read_test.dart`:
- Around line 328-370: Update the two shouldMarkRead override tests to capture
the StreamMarkReadDetails argument and assert its contract instead of only
returning a constant: in the override-blocking test validate the relevant
unread/visibility values, and in the allowing test assert unreadCount is 5,
hasSeenFirstUnreadMessage is false, and isMarkedAsUnread is false. Also add
coverage for an active manual mark-unread followed by a new message, asserting
the details passed through that path.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 871e1dfd-e57c-4606-9922-272ce9385216
📒 Files selected for processing (33)
packages/stream_chat/CHANGELOG.mdpackages/stream_chat/lib/src/client/channel.dartpackages/stream_chat/test/src/client/channel_test.dartpackages/stream_chat_flutter/CHANGELOG.mdpackages/stream_chat_flutter/lib/src/localization/translations.dartpackages/stream_chat_flutter/lib/src/message_list_view/mark_read_details.dartpackages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dartpackages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dartpackages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_view_configuration.dartpackages/stream_chat_flutter/lib/src/message_list_view/unread_indicator_button.dartpackages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dartpackages/stream_chat_flutter/lib/stream_chat_flutter.dartpackages/stream_chat_flutter/test/src/localization/default_translations_test.dartpackages/stream_chat_flutter/test/src/message_list_view/mark_read_test.dartpackages/stream_chat_flutter/test/src/message_list_view/unread_divider_test.dartpackages/stream_chat_flutter/test/src/mocks.dartpackages/stream_chat_flutter_core/CHANGELOG.mdpackages/stream_chat_flutter_core/lib/src/stream_channel.dartpackages/stream_chat_localizations/CHANGELOG.mdpackages/stream_chat_localizations/example/lib/add_new_lang.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dartpackages/stream_chat_localizations/test/translations_test.dartsample_app/lib/routes/app_routes.dart
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2871 +/- ##
==========================================
+ Coverage 72.87% 73.42% +0.54%
==========================================
Files 429 430 +1
Lines 27716 27850 +134
==========================================
+ Hits 20199 20448 +249
+ Misses 7517 7402 -115 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Missed in the previous review-comment pass; it's created alongside the other mark-read/unread notifiers and needs the same teardown.
Submit a pull request
Linear:
fixes FLU-672 (main issue)
fixes FLU-640
fixes FLU-648
fixes FLU-649
fixes FLU-650
Github Issue: #
CLA
Description of the pull request
The requirements changed again while this was being build, so the linear tickets are not right.
Requirements:
StreamChannelobject.Screenshots / Videos
Simulator.Screen.Recording.-.iPhone.17.Pro.Max.-.2026-08-06.at.12.02.06.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.Max.-.2026-08-06.at.12.00.20.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.Max.-.2026-08-06.at.12.00.45.mov
Summary by CodeRabbit
New Features
Bug Fixes