Skip to content

fix(ui, samples): respect asymmetric safe-area insets on iPhone Duo - #3017

Open
xsahil03x wants to merge 19 commits into
masterfrom
sahil/flu-820-respect-asymmetric-safe-area-insets-on-iphone-duo-composer
Open

xsahil03x wants to merge 19 commits into
masterfrom
sahil/flu-820-respect-asymmetric-safe-area-insets-on-iphone-duo-composer

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Resolves FLU-820. On iPhone Duo the status rail sits on one side, so the left and right safe-area insets differ (about 84pt on the right). Several places either ignored the side insets or dropped them.

  • Composer: the side insets no longer animate to zero while the attachment picker opens; only the bottom one gives way. This also stops the picker thumbnails reloading on every frame. The top border moved to the full-width background, so it runs under the rail like the app bar. It's now an inside stroke, matching Figma, which makes the composer 1px shorter.
  • Message list (enableSafeArea): content sits in a SafeArea for the side insets and keeps the top and bottom as scroll padding. Messages, the floating date divider, the unread indicator and the scroll-to-bottom button stay clear of the rail, and the background stays full width. Loading, empty and error states, including custom builders, clear every inset. This replaces the per-widget padding from the earlier commits in this PR.
  • Paged list views: the default empty, loading and error states (StreamScrollViewEmptyWidget, StreamScrollViewErrorWidget, StreamScrollViewLoadingWidget) clear the safe area, and the channel and thread loading rows clear the side insets like StreamListTile. Custom state builders are laid out as before.
  • Sample app: info, config and details screens keep the side insets. The language picker and message info use showStreamSheet.

Video controls in the media viewer depend on fluttercommunity/chewie#972.

Verified on the iPhone Duo simulator (outer display), with new widget tests for each fix.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Message composers respect side safe-area insets, including while the attachment picker is open. The composer’s top border spans the available width and appears above the no-permission message.
    • Message lists and paged list or grid views keep empty, loading, and error states clear of safe-area obstructions. Message-list content and controls account for side insets when safe areas are enabled.
  • Improvements
    • Sample app screens respect side insets, and language selection and message information use consistent sheet layouts.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 180b7337-23aa-47ff-b24d-3a63d7fec994

📥 Commits

Reviewing files that changed from the base of the PR and between 61a3212 and 4e5e92e.

📒 Files selected for processing (3)
  • packages/stream_chat_flutter/CHANGELOG.md
  • sample_app/lib/pages/channel_file_display_screen.dart
  • sample_app/lib/pages/channel_media_display_screen.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stream_chat_flutter/CHANGELOG.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

📝 Walkthrough

The Flutter SDK adjusts safe-area handling for composers, message lists, and paged views. The sample app applies side safe areas to several screens and updates the locale picker and message-info sheet.

Changes: SDK safe-area handling

Layer / File(s) Summary
Composer side insets and surface
packages/stream_chat_flutter/lib/src/message_input/*, packages/stream_chat_flutter/test/src/message_input/*, packages/stream_chat_flutter/CHANGELOG.md
The composer applies left and right insets while the attachment picker opens. The regular surface draws a top border. Tests cover background width and picker animation with a right inset. The changelog records the safe-area changes.
Message-list safe areas
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart, packages/stream_chat_flutter/test/src/message_list_view/*, packages/stream_chat_flutter/CHANGELOG.md, docs/docs_screenshots/test/src/mocks.dart
The message list separates top and bottom scroll padding from side insets. When enableSafeArea is enabled, loading, empty, and error states use safe-area wrappers. Tests cover controls, rows, backgrounds, and states. The screenshot-test mock stubs isMarkedAsUnread.
Paged list and grid states
packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart, packages/stream_chat_flutter_core/test/paged_value_scroll_view_safe_area_test.dart, packages/stream_chat_flutter_core/CHANGELOG.md
Empty, loading, and error states use SafeArea when padding is null. Tests cover right insets and explicit zero padding.

Sample app layout and sheets

Layer / File(s) Summary
Screen side insets
sample_app/lib/config/sample_app_config_screen.dart, sample_app/lib/pages/*
Several screens wrap their existing content in SafeArea with top and bottom insets disabled.
Locale and message-info sheets
sample_app/lib/config/sample_app_config_screen.dart, sample_app/lib/widgets/message_info_sheet.dart
The locale picker and message-info sheet use showStreamSheet, sheet headers, and supplied scroll controllers. The message-info sheet uses themed spacing and list tiles.

Priority: ➖ Normal

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

Change: Bug fix

Suggested reviewers: renefloor

Merge Risk: ⚪ Minimal · up to 4e5e9

No specific user-facing regression is established in the reviewed changes; normal project checks remain appropriate before merging.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 4e5e9

The changes affect where chat content and sheets appear on devices with asymmetric safe areas. The examined media and file screens do not show a new route to content or a change in access controls. The available security review is incomplete, so this is not a finding of zero risk.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The examined entrypoint changes affect client-side presentation, not the channel-scoped queries or preview propagation that determine which media and files these screens display.

Trust Boundaries and Controls

  • observed — The media gallery retains its attachment filter, pagination callback, and preview tap path across the SafeArea change.
Pre-merge checks: Passed 5
Passed Passed checks: 5 passed
Check name Status Explanation
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
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.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly summarizes the main change: updating UI and sample screens to respect asymmetric safe-area insets on iPhone Duo. It is concise and specific.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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 Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.65517% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.13%. Comparing base (8d76439) to head (ebce125).

Files with missing lines Patch % Lines
...r/lib/src/message_list_view/message_list_view.dart 78.57% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3017      +/-   ##
==========================================
+ Coverage   78.56%   79.13%   +0.56%     
==========================================
  Files         452      452              
  Lines       29451    29459       +8     
==========================================
+ Hits        23139    23312     +173     
+ Misses       6312     6147     -165     

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

Comment thread sample_app/lib/pages/advanced_options_page.dart
xsahil03x and others added 11 commits September 29, 2026 22:49
…opens

Opening the inline attachment picker collapsed every safe-area edge toward zero, so on
a device with a side inset (iPhone Duo's status rail, a notched phone in landscape) the
composer and the picker ran under it, and the grid changed width on every animation
frame. Only the bottom inset now gives way to the picker.

The composer's top divider moves from the input row to the full-width background, so it
spans the screen while the input stays inside the safe area.

Refs FLU-820

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With `enableSafeArea` on, the message list applies the insets itself, but the
scroll-to-bottom button's end offset was a fixed margin, so on a device with a side
inset it sat under the system area. The margin is now added on top of the end-side
inset, resolved for the text direction, which lines the button up with the bubbles.

Refs FLU-820

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nsets

The floating date divider and the unread indicator were centered on the list's full
width, while the messages and the inline date dividers sit between the side safe-area
insets. On a device with a side inset they were off-center, and the floating pill no
longer lined up with the inline divider it fades into, so both showed side by side.
Both pills are now padded by the side insets and centered over the list's content.

Refs FLU-820

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rea insets

The Configuration, Advanced options, Chat info, Group info, Group details and New
chat screens padded only the top inset and set a fixed side padding, so on a device
with a side inset (iPhone Duo's status rail, a notched phone in landscape) their
content ran under the system area. Each body is now wrapped in a side-only SafeArea;
the scroll views keep handling the top and bottom.

The language picker and the Message info sheet move from Material bottom sheets to
showStreamSheet with StreamSheetHeader, so the sheets stay clear of the rail too.
Message info's rows use StreamListTile and follow the other sheets' layout.

Refs FLU-820

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…area insets

With `enableSafeArea` on, `StreamMessageListView` now wraps its content in a
`SafeArea` for the side insets and takes only the top and bottom insets as
scroll padding. Messages, the floating date divider, the unread indicator and
the scroll-to-bottom button stay clear of the side insets, while the
background still spans the full width. The loading, empty and error states,
including custom ones from `builders`, clear every inset.

This replaces the per-widget padding from 706b6c6 and 81e49d7.

Also renames the two composer safe-area tests to name the widget under test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Without an explicit `padding`, `PagedValueListView` and `PagedValueGridView`
inset their items by the safe area but drew their empty, loading and error
states under it. Those states now clear every inset; passing a `padding`
still leaves them alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The message list reads it since the unread banners landed, so every docs screenshot with a message list rendered an error screen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…safe-area insets

The Photos & Videos grid ran under a side inset (iPhone Duo's status rail, a notched
phone in landscape), with the system indicators drawn over the thumbnails. The Files
screen's month headers used a fixed side padding, so their labels could run under a
left inset. The grid is now wrapped in a side-only SafeArea, and each header keeps its
full-width background while its label takes the safe-area insets.

Refs FLU-820

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Refs FLU-820

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@xsahil03x
xsahil03x force-pushed the sahil/flu-820-respect-asymmetric-safe-area-insets-on-iphone-duo-composer branch from 4e5e92e to 47e41ed Compare September 29, 2026 20:49
xsahil03x and others added 3 commits October 1, 2026 12:00
# Conflicts:
#	packages/stream_chat_flutter/CHANGELOG.md
#	packages/stream_chat_flutter_core/CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AP6bV4L7FQYjHoHZeomLxr
xsahil03x added a commit that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AP6bV4L7FQYjHoHZeomLxr
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AP6bV4L7FQYjHoHZeomLxr
xsahil03x added a commit that referenced this pull request Oct 12, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AP6bV4L7FQYjHoHZeomLxr

@renefloor renefloor 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.

Reviewed this layer on its own (the rest of the stack gets separate reviews). The composer fix and the message-list safe-area tests look good — they assert real geometry and most would fail on revert. A few things inline, mainly the paged-list SafeArea, which I think is a behavioural change rather than a fix.

// Without a padding, a non-scrolling state clears every safe-area inset.
Widget _insetState(Widget child) {
if (widget.padding != null) return child;
return SafeArea(child: child);

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.

This wraps the customer's emptyBuilder / loadingBuilder / errorBuilder output in a SafeArea whenever padding is null, which is the default for StreamChannelListView, StreamThreadListView, the member/user/poll-vote lists, etc. (same in PagedValueGridView below).

A customer whose empty state is a full-bleed illustration or coloured background will see it inset after upgrading, with no compile warning, and the only opt-out (passing padding) also drops the items' automatic scroll inset. Per CLAUDE.md I'd treat this as a behavioural change: list it under 🔄 Changed (not 🐞 Fixed) with the opt-out spelled out, and mention it in the padding dartdoc.

It's also wider than what the items get: a vertical ListView with padding: null only takes the top/bottom insets, but this takes all four sides. E.g. a short horizontal strip at the top of a body without an AppBar now loses ~47pt at the top only in its empty/loading state. Consider only applying the main-axis insets, to match BoxScrollView.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed it shouldn't touch customer builders. In 954d4a0 I moved the inset out of PagedValueListView/PagedValueGridView and into our own default widgets: StreamScrollViewEmptyWidget, StreamScrollViewErrorWidget and StreamScrollViewLoadingWidget clear the safe area via StreamSafeArea(minimum: …), and the channel and thread skeleton rows clear the side insets the way StreamListTile does (top: false, bottom: false). Custom emptyBuilder/loadingBuilder/errorBuilder output is laid out exactly as before, with a test for that.

I didn't go main-axis only: the Duo's rail is a cross-axis inset, and BoxScrollView doesn't drop the cross-axis insets either. It passes them down through MediaQuery for each item to clear, which is what the rows now do.

🐞 Fixed

- Fixed `StreamDraftListController.deleteDraft` removing the wrong draft.
- Fixed `PagedValueListView` and `PagedValueGridView` drawing their empty, loading and error states under the safe area when no `padding` is given.

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.

See the comment on _insetState: this changes how customer-supplied state builders are laid out, so I think it belongs under 🔄 Changed, with the opt-out (pass a padding).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Core no longer changes in this PR (954d4a0), so the entry is gone. The UI changelog has a 🐞 Fixed line for the default states instead.

expect(_backgroundFillFinder(tester), findsOneWidget);
});

testWidgets('StreamMessageComposer runs the regular top divider under the side insets', (tester) async {

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.

I think this passes for the wrong reason: the fill DecoratedBox always spanned the full width, so the width assertion was already true before this PR. It only fails on revert because _backgroundFillFinder now matches the exact BoxDecoration including the border.

It wouldn't catch the border being re-added inside StreamChatMessageInput (double border). Could it assert that no bordered box sits under StreamChatMessageInput, and that the bordered box starts at x=0?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, confirmed: the old assertion passed with the change reverted, and a second border inside StreamChatMessageInput passed every composer test. In c5f42ff the test finds every top-only border inside the composer: one test checks it runs from x=0 to the full width under a left and right inset, and another checks there is exactly one. Both regressions now fail.

🔄 Changed

- `StreamMessageListView` now rebuilds only the message rows that changed; a custom row builder should read changing outside state through an inherited widget.
- `StreamMessageComposer`'s top border now spans the full width, including under the side safe-area insets, and also shows above the no-permission message.

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.

The PR description says the composer is now 1px shorter because the border became an inside stroke. Worth adding to this line: integrators with golden tests will see diffs, as this PR's own goldens show.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added in 8b93903.

});

testWidgets('StreamMessageListView centers the floating date divider over the content area', (tester) async {
const padding = EdgeInsets.only(right: 84);

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.

All the safe-area tests in this PR use a right-only inset. A swapped left/right (e.g. in StreamSafeArea.driven(to: ...) in the composer) or a symmetric horizontal / 2 would still pass. Could we add a case with asymmetric insets on both sides, e.g. EdgeInsets.only(left: 47, right: 84), and assert both edges, for the message list and for the composer while the picker opens?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in c5f42ff: the message list and composer tests now use left: 47, right: 84 and check both edges. Swapped or averaged insets were already caught by the composer test (it compares against the closed state), but dropping the left side wasn't, and neither was copyWith(right: 0) in the message list. Both fail now.

xsahil03x and others added 4 commits October 15, 2026 15:25
…uilders

Moves the safe-area inset out of PagedValueListView and PagedValueGridView,
where it wrapped customer emptyBuilder/loadingBuilder/errorBuilder output,
into the SDK's own state widgets. StreamScrollViewEmptyWidget,
StreamScrollViewErrorWidget and StreamScrollViewLoadingWidget clear the safe
area with StreamSafeArea(minimum: ...), and the channel and thread skeleton
rows clear the side insets the way StreamListTile does. Custom builders are
laid out as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Names the list views a customer sees in the changelog instead of the
internal skeleton widgets, says what "safe area" means in the state
widgets' docs, notes the composer is 1px shorter, and drops a private
comment that restated its code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The message list and composer safe-area tests use different left and right
insets and check both edges, the composer divider test fails on a second
border or one inside the insets, and the safe-area tests build their own
fixtures instead of sharing them through setUp.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/stream_chat_flutter/CHANGELOG.md
@xsahil03x xsahil03x changed the title fix(ui, core, samples): respect asymmetric safe-area insets on iPhone Duo fix(ui, samples): respect asymmetric safe-area insets on iPhone Duo Oct 15, 2026

This branch has not been deployed

No deployments
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