Skip to content

feat(ui): add configurable StreamBackButton unread count#2816

Open
VelikovPetar wants to merge 5 commits into
masterfrom
feature/FLU-557_fix_back_button_unread_counts
Open

feat(ui): add configurable StreamBackButton unread count#2816
VelikovPetar wants to merge 5 commits into
masterfrom
feature/FLU-557_fix_back_button_unread_counts

Conversation

@VelikovPetar

@VelikovPetar VelikovPetar commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Submit a pull request

Linear: FLU-557

Github Issue: #

CLA

  • I have signed the Stream CLA (required).
  • The code changes follow best practices
  • Code changes are tested (add some information if not applicable)

Description of the pull request

The StreamChannelHeader back-button unread badge counted the currently open channel in its total, so it never reflected "unread elsewhere".

  • StreamBackButton now takes an unreadCount configuration via the new StreamBackButtonUnreadCount:
    • .total({excludeCid}) — total unread across channels, optionally excluding one channel.
    • .channel(cid) — a specific channel's unread count.
  • StreamChannelHeader now uses .total(excludeCid: channel.cid), so its back-button badge shows the unread messages waiting in other channels.
  • StreamUnreadIndicator gained an opt-in excludeCid (bare usage is unchanged).
  • showUnreadCount and channelId on StreamBackButton are deprecated but still functional — fully backwards compatible.

Test instructions: open a channel that has unread messages while other channels also have unread. The header back-button badge shows total − thisChannel; going back to the channel list, the badge shows the full total. Covered by unit tests in back_button_test.dart and unread_indicator_test.dart (incl. the deprecated paths).

Screenshots / Videos

Before After
unreads-before.mov
unreads-after.mov

Summary by CodeRabbit

  • New Features

    • Added unreadIndicator support for back buttons to render unread badges using total counts or per-channel counts.
    • Updated channel and thread headers to drive unread badges via unreadIndicator.
  • Bug Fixes

    • Unread totals now exclude the currently open channel, with subtraction clamped to zero.
  • Documentation

    • Added migration guidance and updated changelog notes for the unreadIndicator migration and legacy unread options deprecation.
  • Tests

    • Expanded widget and golden coverage for unread badge rendering, including excludeCid behavior and RTL snapshots.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 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

Run ID: f2b940cd-3c33-40f2-8d24-a691a233452a

📥 Commits

Reviewing files that changed from the base of the PR and between 4f3aa78 and afaafde.

⛔ Files ignored due to path filters (6)
  • packages/stream_chat_flutter/test/src/indicators/goldens/ci/stream_unread_indicator_dark.png is excluded by !**/*.png
  • packages/stream_chat_flutter/test/src/indicators/goldens/ci/stream_unread_indicator_light.png is excluded by !**/*.png
  • packages/stream_chat_flutter/test/src/indicators/goldens/ci/stream_unread_indicator_overflow_light.png is excluded by !**/*.png
  • packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_back_button_no_unread_light.png is excluded by !**/*.png
  • packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_back_button_unread_dark.png is excluded by !**/*.png
  • packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_back_button_unread_light.png is excluded by !**/*.png
📒 Files selected for processing (1)
  • packages/stream_chat_flutter/test/src/misc/back_button_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stream_chat_flutter/test/src/misc/back_button_test.dart

📝 Walkthrough

Walkthrough

StreamBackButton now accepts explicit unread indicators, supports excluding the current channel from total counts, updates header integrations, retains deprecated flags, and adds tests and migration documentation.

Changes

Unread badge migration

Layer / File(s) Summary
Excluded total unread calculation
packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart, packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart
StreamUnreadIndicator subtracts an optional channel count from total unread values, clamps negative results to zero, and handles missing channels.
Explicit back-button unread API
packages/stream_chat_flutter/lib/src/misc/back_button.dart, packages/stream_chat_flutter/test/src/misc/back_button_test.dart
StreamBackButton accepts unreadIndicator, overlays the resolved indicator, preserves deprecated flags as fallback behavior, and validates supported configurations.
Header and example integrations
packages/stream_chat_flutter/lib/src/channel/channel_header.dart, packages/stream_chat_flutter/lib/src/misc/thread_header.dart, packages/stream_chat_flutter/example/lib/main.dart, packages/stream_chat_flutter/test/src/channel/channel_header_test.dart, docs/docs_screenshots/test/localization/localization_rtl_test.dart
Headers and the example pass explicit unread indicators, including exclusion of the active channel in the default channel header.
Migration and release documentation
migrations/redesign/headers_and_icons.md, packages/stream_chat_flutter/CHANGELOG.md
Documentation describes the new API, deprecated parameters, and current-channel exclusion behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Header
  participant StreamBackButton
  participant StreamUnreadIndicator
  participant ClientState
  Header->>StreamBackButton: configure unreadIndicator
  StreamBackButton->>StreamUnreadIndicator: resolve total or channel indicator
  StreamUnreadIndicator->>ClientState: read unread streams
  ClientState-->>StreamUnreadIndicator: provide unread counts
  StreamUnreadIndicator-->>StreamBackButton: render badge
Loading

Possibly related PRs

Suggested reviewers: xsahil03x

🚥 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 accurately summarizes the main UI change: making StreamBackButton unread count configurable.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/FLU-557_fix_back_button_unread_counts

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.

@VelikovPetar VelikovPetar changed the title feat(ui): add configurable StreamBackButton unread count feat(ui): add configurable StreamBackButton unread count Jul 14, 2026
///
/// Set [excludeCid] to omit a channel's unread messages from the total -
/// for example, the currently open channel.
const factory StreamBackButtonUnreadCount.total({String? excludeCid}) = _TotalUnreadCount;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not completely sure about the naming here, let me know if you think we should use something different (.unreadMessages()perhaps, to serve as a forecast for the potentially upcoming .unreadChannels())

@VelikovPetar
VelikovPetar marked this pull request as ready for review July 14, 2026 19:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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/example/lib/main.dart`:
- Around line 228-232: Update the unread-count selection in the back-button
setup to use the total count excluding the current channel rather than
StreamBackButtonUnreadCount.channel(cid). Preserve the existing fallback
behavior when no channel CID is available.
🪄 Autofix (Beta)

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

Run ID: d4e9d622-7b75-4baf-801c-03086903330e

📥 Commits

Reviewing files that changed from the base of the PR and between 5c4c5a7 and a1f86e7.

📒 Files selected for processing (11)
  • docs/docs_screenshots/test/localization/localization_rtl_test.dart
  • migrations/redesign/headers_and_icons.md
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/example/lib/main.dart
  • packages/stream_chat_flutter/lib/src/channel/channel_header.dart
  • packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart
  • packages/stream_chat_flutter/lib/src/misc/back_button.dart
  • packages/stream_chat_flutter/lib/src/misc/thread_header.dart
  • packages/stream_chat_flutter/test/src/channel/channel_header_test.dart
  • packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart
  • packages/stream_chat_flutter/test/src/misc/back_button_test.dart

Comment thread packages/stream_chat_flutter/example/lib/main.dart Outdated
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.55172% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 71.37%. Comparing base (4dee989) to head (afaafde).

Files with missing lines Patch % Lines
...t_flutter/lib/src/indicators/unread_indicator.dart 92.85% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2816      +/-   ##
==========================================
+ Coverage   71.35%   71.37%   +0.02%     
==========================================
  Files         430      430              
  Lines       26945    26965      +20     
==========================================
+ Hits        19226    19246      +20     
  Misses       7719     7719              

☔ 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.

final String? channelId;

/// The unread count configuration for the back button.
final StreamBackButtonUnreadCount? unreadCount;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we instead just ask for a Widget/StreamUnreadIndicator here directly? It will make it more composable and the api cleaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, perhaps passing a pure Widget would be the most future-proof. Not sure if we would be opening the API a bit too much, but since it is a pattern already used in the SDK, I think we should be OK. I will draft a solution using a Widget instead of StreamBackButtonUnreadCount.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a small caveat with this approach: If we rework it to pass a Widget? unreadIndicator, passing a StreamUnreadIndicator complicates the setup a bit because currently, its placement is done by nesting a child:

StreamUnreadIndicator(
      offset: .zero,
      excludeCid: excludeCid,
      child: button, // <--- StreamBackButton
)

This would mean that when building a StreamBackButton with a StreamUnreadIndicator, We will have to pass child: null in the indicator, and handle the positioning inside the StreamBackButton, something along the lines of:

if (_effectiveUnreadIndicator case final ind?) {                                                                         
    button = Stack(                                                                                                                          
      clipBehavior: Clip.none,                                                                                                                                      
      children: [                                                                                                                                                          
        button,                                                                                                                     
        Positioned.fill(                                                                                                      
          child: FittedBox(                                                                                                      
            fit: BoxFit.none,                                                                                                                          
            alignment: AlignmentDirectional.topEnd,                                                                                                                                 
            child: ind,              // <---- StreamUnreadIndicator WITHOUT child                                                                                                                                         
          ),                                                                                                                                                                      
        ),                                                                                                                                                                       
      ],                                                                                                                                                                         
    );                                                                                                                                                                       
  }

Should be doable, but in my opinion, it makes the API pretty confusing. I wanted to bring this up, before committing to this solution.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be good, just make sure we use the same position as we had before. Flutter Badge also supports a nullable child.

https://api.flutter.dev/flutter/material/Badge/child.html

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: 4f3aa78

this.semanticLabel,
}) : _unreadType = _UnreadChannels(cid: cid);
}) : _unreadType = _UnreadChannels(cid: cid),
excludeCid = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can support excludeCid here too. wdyt?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory yes, should be feasible. I decided to not do it now because it wasn't part of the initial bug report. However I have one concern: In that case, we could in theory pass both cid and excludeCid to the _UnreadChannels config -> This can become a bit confusing, but if we prioritize the setup in the following way:

  1. If cid != null -> show that specific channel unread count
  2. If excludeCid != null -> all unread channels - 1 (if excluded channel has unread)
  3. Else -> all unread channels

(Ideally we would have different factory methods for channels vs currentChannel, but currently the channels factory handles both cases, so I think introducing new API might be confusing).

What do you think about this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets skip it until we have a usecase

VelikovPetar and others added 4 commits July 21, 2026 19:53
# Conflicts:
#	packages/stream_chat_flutter/CHANGELOG.md
Replace the typed StreamBackButtonUnreadCount config with a plain
unreadIndicator Widget (typically a StreamUnreadIndicator), per review
feedback on #2816. StreamBackButton now overlays the indicator on its
top-end corner and hides it at zero count; badge placement is unchanged,
verified pixel-identical via new golden coverage for the back button and
the unread indicator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an explicit widget-level assertion that a configured unreadIndicator
renders the StreamUnreadIndicator but no StreamBadgeNotification when the
count is zero, complementing the existing golden coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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