Skip to content

feat(ui, samples): two-pane channel list and channel on wide windows and foldables - #3023

Open
xsahil03x wants to merge 38 commits into
sahil/flu-823-iphone-duo-polish-viewer-fit-thumbnails-autocomplete-heightfrom
sahil/flu-826-sample-app-two-pane-channel-list-and-channel-on-the-inner
Open

xsahil03x wants to merge 38 commits into
sahil/flu-823-iphone-duo-polish-viewer-fit-thumbnails-autocomplete-heightfrom
sahil/flu-826-sample-app-two-pane-channel-list-and-channel-on-the-inner

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Part of FLU-826. Stacked on #3021 (FLU-823); review only the commits on top of it.

On a wide window the sample app now shows the channel list and the open channel side by side, and it keeps one stack on phones.

Sample app

  • AdaptiveSplitView (in widgets/split_view.dart) shows two panes when the window is at least expanded wide (840) and medium tall (480), using Material's width and height classes (utils/window_size_class.dart). That covers iPhone Duo's inner display in landscape, the Pixel Fold in either orientation, and tablets in landscape. On narrower windows the chat routes stay one stack.
    • Apple: split view columns on iPhone Duo "appear both tiled and as overlays" when open, and single-view in portrait.
    • Material: two panes at expanded width (840 and up), one recommended below that.
  • The channel routes live in one ShellRoute navigator in both layouts, so an open channel keeps its draft and scroll position across a fold, unfold or rotation. Only the list moves between layouts, by a GlobalKey.
  • A hinge or half-open fold (MediaQuery.displayFeatures) splits the panes at the fold. Each pane gets its own insets and display features via removeDisplayFeatures, and the divider eases into place.
  • People can resize the panes by dragging the handle in the middle of the divider (a mouse or trackpad can drag anywhere along it), with the arrow keys, or with a screen reader's adjust actions. Each pane takes a SplitPaneConstraints(minWidth:, initialWidth:, maxWidth:), SwiftUI's navigationSplitViewColumnWidth(min:ideal:max:) under Flutter's names. The list starts at 320 (UIKit's default primary column), within 280 to 420, and the chat keeps at least 360. While a fold divides the window, the divider can't be dragged.
  • In the split:
    • Picking a channel swaps it in place without a transition, as UISplitViewController does when regular. Drill-ins such as chat info still push within the pane.
    • The chat has no back button.
    • The open channel, thread, draft or reminder is highlighted.
    • The drawer covers both panes, since a modal drawer is meant to block the whole app.
  • The Threads and Drafts tabs used to push onto the nearest navigator, which covered both panes. Every tab now opens the channel route.
  • Fixes found along the way:
    • A channel's composer text leaked into the next channel opened in place.
    • A channel's menus stayed open over the next one.
    • The selected tile drifted while scrolling and showed behind the search field.

SDK (stream_chat_flutter)

  • Added automaticallyImplyLeading to StreamChannelPage and StreamThreadPage.
  • Added a selected parameter and copyWith to StreamThreadListTile.
  • Fixed StreamThreadListTile overflowing when narrow; the timestamp now truncates.
  • Fixed StreamMessageItem's actions modal throwing when its message left the tree while it was open.

Verified

  • On the iPhone Duo simulator: outer display, inner landscape and portrait, and Split View multitasking on both halves.
  • On the Pixel 10 Pro Fold emulator: flat, half-open and folded, in both orientations.
  • Resizing on web (pointer, keyboard, limits, window resize) and on the iPhone Duo simulator (touch, fold snapping and its animation).
  • Checked on both: state across folds, draft isolation, drawer, menus, back and search, and the phone flow with default transitions.
  • Tests cover the new SDK parameters and the two SDK fixes. The overflow and modal regression tests fail without their fixes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Channel and thread pages can hide their back buttons when needed.
    • The sample app adapts to window size, displaying chats and conversations side by side on larger screens and a “Select a chat” placeholder when appropriate.
    • Drafts, threads, reminders, and channels highlight the selected item and update the displayed conversation.
  • Bug Fixes
    • Improved timestamp display in narrow thread tiles and prevented message action menus from failing if a message leaves the screen.
  • Improvements
    • Added copyWith support for thread and draft tiles.

xsahil03x and others added 19 commits September 29, 2026 23:36
…lar windows

On a window that is regular on both axes, such as an unfolded foldable or a
tablet, the channel list now sits in a 320pt leading pane beside the channel.
Compact windows keep the single stack. The channel routes live in one shell
navigator in both layouts, so an open channel keeps its draft and scroll
position across a fold or unfold; only the list moves, by a GlobalKey.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a hinge, or a half-open fold, divides the window into a left and a right
side, each pane now fills one side instead of the list keeping its 320pt
width. The fold is read from MediaQuery.displayFeatures, so this works on
Android foldables today. Each pane gets its own MediaQuery via
removeDisplayFeatures, which also replaces the manual padding trimming.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Gradle 8.14 does not run on Java 25, which current Android Studio bundles.
9.3.1 is the newest Gradle Flutter 3.47's tooling supports and still works
with AGP 8.12.

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

Passes through to the header, so a page shown as the detail pane of a split
view can hide its back button. Defaults to true, keeping the back button.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
While the list and channel are side by side, the channel has no back button,
the open channel is highlighted in the list, and opening a channel from the
list replaces the one shown instead of stacking on it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
go_router keys the channel page by its route pattern, so replacing one open
channel with another reused the same page: the composer kept the previous
channel's text and saved it as the new channel's draft. The page is now
keyed by channel and thread, so each gets a fresh one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Drafts tab pushed its channel on the nearest navigator, which beside the
list is the root one, so the channel covered both panes. It now opens the
channel route like the other tabs, passing the thread's parent along for
threads that are not loaded in the channel yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Class and member docs start with noun phrases, the split view links the size
classes it names and shows how to wire it into a ShellRoute, and the docs say
when a lookup rebuilds its caller. Constants used by one type move onto it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The list's scroll listener, which only hides the keyboard, consumed every
scroll notification. The Material the selected tile paints on never heard
about the scroll, so the highlight lagged behind its tile. The listener now
lets the notifications through.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tile's bottom row held the participants, reply count and timestamp at
their natural widths, so a narrow list or large text overflowed it. The
timestamp now gives way and truncates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A selected tile blends the selection color over its background and reports
itself as selected to assistive technologies. copyWith lets an itemBuilder
mark the tile without rebuilding it from scratch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Threads tab pushed its thread on the nearest navigator, which beside the
list covers both panes. It now opens the channel route like the other tabs.
The route's thread branch takes over keeping the parent message current and
"View in channel". Threads, drafts and reminders are highlighted while open
beside the list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A modal drawer blocks the rest of the app behind a scrim, but beside the
channel it only covered the list pane. The split view now hosts the drawer
over both panes, keeping only the inset of the edge it slides in from. On a
phone the list page keeps its own drawer.

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

The modal's builder read the chat configuration from the message's context.
When the message left the tree while the modal was open, for example when
another channel opened beside the list, the next rebuild of the modal
looked up a deactivated context and threw. The configuration is now read
before the modal opens.

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

The channel page is now keyed by channel and thread, so opening another one
replaces the page and closes the menus open on it. Pages in the channel
pane swap in without a transition while the list is beside them, and keep
the platform's page transition on a phone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A regular window now needs to be at least 840 wide before the channel list
and channel sit side by side, so an iPhone Duo's inner display in portrait
or a tablet in portrait shows one pane at a time, while wider windows keep
the split.

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

The selected tile paints its background on the nearest Material, which was
the page's, so it stayed visible behind the search field once the tile
scrolled under it. A transparent Material around the list keeps it inside
the list.

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

The list pane used to jump between its own width and the fold. It now eases
there over 200ms, or instantly when animations are disabled. Only its width
animates, so rotating or resizing the window still lays out at once.

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

coderabbitai Bot commented Sep 30, 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: c4c2a5cd-e0b2-482f-84ee-77ade145c8df

📥 Commits

Reviewing files that changed from the base of the PR and between 5f43140 and c590111.

📒 Files selected for processing (1)
  • sample_app/lib/widgets/split_view.dart

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

📝 Walkthrough

The sample app adds adaptive split-view layouts, route-based channel and thread navigation, and route-driven list selection. The Stream Chat package adds configurable channel and thread page back buttons, selected thread tiles, and a message-action modal configuration update.

Changes: Adaptive split-view navigation

Layer / File(s) Summary
Split-view layout foundation
sample_app/lib/utils/window_size_class.dart, sample_app/lib/widgets/split_view.dart, sample_app/lib/pages/channel_placeholder_page.dart, sample_app/android/gradle/wrapper/gradle-wrapper.properties, sample_app/android/settings.gradle
The sample app adds window-size classes, split-view widgets, adaptive route pages, and a channel placeholder. The Gradle wrapper and Crashlytics plugin versions change.
Adaptive channel and thread routes
sample_app/lib/routes/app_routes.dart, sample_app/lib/pages/choose_user_page.dart, packages/stream_chat_flutter/lib/src/pages/channel_page.dart, packages/stream_chat_flutter/lib/src/pages/thread_page.dart, packages/stream_chat_flutter/test/src/channel/*
Channel routes use an adaptive split-view shell and resolve thread parents from route extras or channel state. Channel and thread pages add automaticallyImplyLeading, defaulting to true, with tests for both settings. User selection pushes and conditionally removes a progress dialog route.
Route-driven lists and selection
sample_app/lib/pages/channel_list_page.dart, sample_app/lib/pages/draft_list_page.dart, sample_app/lib/pages/reminders_page.dart, sample_app/lib/pages/thread_list_page.dart, sample_app/lib/widgets/channel_list.dart, sample_app/lib/widgets/stream_draft_list_tile.dart
Lists read route state in expanded layouts, navigate through named channel routes, and mark matching items selected. Draft and reminder tiles render selected backgrounds.

Stream Chat page and tile updates

Layer / File(s) Summary
Channel and thread page back-button controls
packages/stream_chat_flutter/lib/src/pages/channel_page.dart, packages/stream_chat_flutter/lib/src/pages/thread_page.dart, packages/stream_chat_flutter/test/src/channel/*
Channel and thread pages add automaticallyImplyLeading, defaulting to true, and pass it to their headers. Tests cover the default and disabled settings.
Thread-tile selection and narrow layout
packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart, packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_tile_test.dart, packages/stream_chat_flutter/CHANGELOG.md
Thread tiles add selected state and copyWith, use selected-state background coloring, and wrap the timestamp in Flexible. Tests cover selection, semantics, copy behavior, and narrow layouts. The changelog records these updates and the page options.
Message-action modal configuration
packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart, packages/stream_chat_flutter/test/src/message_widget/stream_message_item_actions_modal_test.dart, packages/stream_chat_flutter/CHANGELOG.md
The modal uses configuration captured before it opens. A test removes the message while the modal is open and checks for exceptions.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ChannelList
  participant GoRouter
  participant AdaptiveSplitView
  participant ChannelRoute
  participant ChannelPage
  participant ThreadPage
  ChannelList->>GoRouter: Navigate with channel route parameters
  GoRouter->>AdaptiveSplitView: Build channel route in split-view shell
  GoRouter->>ChannelRoute: Provide route parameters and extra
  ChannelRoute->>ThreadPage: Build when a parent message is resolved
  ChannelRoute->>ChannelPage: Build when no parent message is resolved
Loading
Merge Risk: ⚪ Minimal · up to c5901

The inspected split layout and phone navigation preserve their intended behavior. No identified issue blocks merging after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to c5901

The inspected changes remain within the existing signed-in chat client and presentation layer. No new authorization bypass or privileged action path was established. Residual risk concerns preserving channel-specific state and dismissing overlays during interrupted navigation, resizing, and account changes; those runtime behaviors were not fully verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected entrypoint changes affect sample-app navigation and channel/thread UI state, plus SDK consumers opting into presentation controls. They continue to use the current chat client rather than introducing a separate credential or service authority. Effective access to channel data still depends on existing client and server enforcement, which was not fully inspected.

Trust Boundaries and Controls

  • observed — The existing router redirects unauthenticated users away from chat routes. Externally representable CID, mid, and pid values remain navigation inputs to the current client-backed channel route; the split layout and hidden back button do not replace that authentication gate or establish channel authorization themselves.

Resilience and Maintainability Implications

  • observed — Expanded mode places its drawer controller above both panes; collapsed mode removes that controller and gives the channel-list scaffold its drawer. Existing disconnect ordering publishes Unauthenticated and waits for the end of frame before disconnecting the client, supporting teardown of auth-gated UI before channel state disposal. Actual barrier behavior and interrupted teardown were not runtime-tested.

Hardening Proposals

  • proposed — Validate channel-data isolation with transition-focused regression cases combining rapid resize, repeated channel/thread selection, open overlays, and account teardown. Assert that composer state remains bound to the intended channel/thread and stale overlays cannot expose or act on the previous account's state.
Pre-merge checks: Passed 5
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 and concisely describes the main change: adding a two-pane channel list and channel layout for wide windows and foldable devices.
✨ 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.

@xsahil03x
xsahil03x added this pull request to stack #3022 September 30, 2026 00:23
@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 3 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (sahil/flu-823-iphone-duo-polish-viewer-fit-thumbnails-autocomplete-height@516a738). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...er/lib/src/message_widget/stream_message_item.dart 50.00% 2 Missing ⚠️
...ew/thread_scroll_view/stream_thread_list_tile.dart 94.44% 1 Missing ⚠️
Additional details and impacted files
@@                                             Coverage Diff                                              @@
##             sahil/flu-823-iphone-duo-polish-viewer-fit-thumbnails-autocomplete-height    #3023   +/-   ##
============================================================================================================
  Coverage                                                                             ?   79.68%           
============================================================================================================
  Files                                                                                ?      453           
  Lines                                                                                ?    29491           
  Branches                                                                             ?        0           
============================================================================================================
  Hits                                                                                 ?    23501           
  Misses                                                                               ?     5990           
  Partials                                                                             ?        0           

☔ 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 and others added 5 commits September 30, 2026 13:02
…er to pop

Logging in pops the progress dialog through the router once connect
returns. By then the redirect has moved the router onto the channel
shell, whose navigator isn't built yet, and the router's pop throws.
The dialog is now removed through the navigator it was pushed on, and
only if the redirect hasn't already removed it with the login page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Crashlytics 2.8.1 reads the app id with groovy.util.XmlSlurper, which
Gradle 9 no longer ships, so release builds failed while uploading the
mapping file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WindowSizeClass was compact or regular on both axes, and the split view
added its own 840 check on top. It now pairs a WindowWidthClass (compact,
medium, expanded, large, extraLarge) with a WindowHeightClass (compact,
medium, expanded), at the Material breakpoints.

The split view shows both panes when the window is at least expanded wide
and medium tall. The iPhone Duo and Android foldables split as before; a
window at least 840 wide and 480 to 599 tall now splits too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AP6bV4L7FQYjHoHZeomLxr
…utocomplete-height' into sahil/flu-826-sample-app-two-pane-channel-list-and-channel-on-the-inner
Drag the divider, use the arrow keys, or adjust it with a screen reader
to resize the channel list between 280 and 420 while the chat keeps at
least 360. While a fold divides the window the panes stay on either side
of it and the divider can't be dragged.

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 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 SDK changes are small, non-breaking, and each has a test that fails without it 👍. The split view design is nice: a single ShellRoute navigator in both layouts, with only the list moved by a GlobalKey, so a channel keeps its draft and scroll position across fold, unfold and rotation. The divider is also accessible via keyboard and screen reader.

Comments are all on the sample app; one is a real bug (Android back with the drawer open).

Comment thread sample_app/lib/widgets/split_view.dart Outdated
context: context,
removeLeft: !isLtr,
removeRight: isLtr,
child: DrawerController(

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.

In the split layout this DrawerController sits above the ShellRoute's nested navigator, so when it opens, its LocalHistoryEntry is registered on the shell page in the root navigator. go_router's popRoute → _findCurrentNavigator picks the deepest navigator that canPop(), which is the nested one whenever a channel is open.

So: tablet in landscape, channel open, tap the avatar to open the drawer, press system back. I'd expect the channel to close to the placeholder while the drawer stays open. (Inferred from reading go_router's source; I haven't reproduced it on a device.) One-pane mode is fine because the Scaffold drawer is inside the nested navigator.

Fix: a PopScope(canPop: false, …) or BackButtonListener that closes the drawer while it's open.

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, reproduced on a Pixel Fold emulator. go_router sends back to the innermost navigator that can pop, so the channel pane's navigator closed the channel before the drawer's history entry on the shell page was ever asked (still the case in go_router 18). PopScope registers on that same shell page, so it wouldn't help. Fixed in 73d0bd5 with a BackButtonListener around the drawer that closes it first: it hears the back press before the router picks a navigator.

Comment thread sample_app/lib/widgets/split_view.dart Outdated
static const _transitionDuration = Duration(milliseconds: 200);

// The width people resized [SplitView.primary] to, if they have.
double? _resizedPrimaryWidth;

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.

_resizedPrimaryWidth lives in _SplitViewState, which is disposed when AdaptiveSplitView drops to one pane. So drag the list to 420, rotate to portrait and back, and it's 320 again. Could the width live in _AdaptiveSplitViewState and be passed into SplitView (initial width plus a changed callback)?

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.

Fixed in 4ac15ab: AdaptiveSplitView now keeps the resized width, since it outlives the one-pane layout, and hands it back to SplitView through resizedPrimaryWidth / onPrimaryWidthChanged. Checked on the Fold by dragging, folding to one pane and unfolding.

});

// The width of the area, centered on the line, that responds to a drag.
static const hitWidth = 44.0;

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 44px hit area is a translucent GestureDetector on top of both panes, so its horizontal drag recognizer wins over the channel tiles' Slidable (swipes starting in the rightmost 22px of the list) and over swipe-to-reply near the channel pane's leading edge. Maybe ~16px, or use the wide hit area only for mouse input?

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.

Reproduced both cases on a Pixel Fold. Fixed in febffee by following Material 3's pane drag handle: touch now resizes only from a 4×48 handle in the middle of a 24dp space around the line (it grows to 12×52 while held), so row swipes and swipe-to-reply next to the line reach the panes. A mouse or trackpad can still drag anywhere along it.


/// The widths a [SplitView] pane starts at and can be resized within.
@immutable
class SplitPaneConstraints {

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.

nit: SplitPaneConstraints supports initialWidth/maxWidth on the secondary pane plus ==/hashCode/toString, but only secondaryConstraints.minWidth is ever used. For a sample, plain primaryMin/primaryInitial/primaryMax + secondaryMin parameters would cut ~50 lines and the foundation import.

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.

Keeping it. SplitView is written as a reusable widget, and people copy sample widgets into their own apps. The constraints are symmetric, so a caller can size either pane: a fixed-width detail pane with secondaryConstraints.initialWidth, or a capped one with maxWidth. The sample just happens to use only the primary side. Grouping the min, initial and max widths per pane also keeps those values, and the asserts that check them, together.

zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip

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 Gradle 8.14 → 9.3 bump (and Crashlytics 2.8 → 3.0 in settings.gradle) is a toolchain change for every contributor and unrelated to the two-pane layout. Could it go in its own PR so it can be reverted independently?

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.

Split out into #3067 with the minimum versions that work (Gradle 9.1.0, the first that runs on Java 25, and Crashlytics plugin 3.0.0), and reverted here in d9d02db / 8ff68d0.

FocusScope.of(context).unfocus();
}
return true;
return false;

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.

return true → false means scroll notifications now bubble up past this listener. What's the reason? If it's intentional, a short comment would help; otherwise I'd revert it.

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.

It was intentional, in 1b1fbbc: the listener only dismisses the keyboard, but returning true swallowed every scroll notification, so the Material the selected tile paints on never heard about the scroll and the highlight lagged behind its row. 81baeb5 later added a transparent Material inside the list, which now gets the notifications first, so the highlight no longer depends on this. I've kept false anyway, since the listener only observes the scroll and shouldn't hide it from anything above.

xsahil03x and others added 4 commits October 16, 2026 01:48
In the two-pane layout the drawer sits above the channel pane's navigator,
which takes the back press first and closed the channel while the drawer
stayed open. A BackButtonListener now closes an open drawer first.

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

The width people dragged the channel list to lived in the split view, which
is removed while the window shows one pane, so rotating or folding reset it.
AdaptiveSplitView now keeps it and hands it back when the split returns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The divider's 44px drag area sat over both panes and took horizontal swipes
from the channel rows and the messages near it. Touch now resizes from a
handle at the middle of a 24dp space around the line, which grows while
held; a mouse or trackpad can still drag anywhere along it. The panes'
background also paints behind the split, so their edges leave no seam.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
// nothing carries over from the last one: neither its state, such
// as the composer's text, nor the menus open on it.
return AdaptiveSplitViewPage<void>(
key: ValueKey((channel.cid, parentMessage?.id)),

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 fixed key breaks the two call sites that still open the channel with pushNamed. They were not switched to goNamed with the others:

  • sample_app/lib/app.dart:96, tapping a push notification
  • sample_app/lib/widgets/all_members_sheet.dart:444, opening a DM from the members sheet

Before, each push got a unique state.pageKey. Now:

  • Same channel already open: the shell navigator ends up with two pages keyed (cid, null), and in debug builds Navigator's duplicate-key assertion fails.
  • Split view: the pushed page stacks inside the chat pane with automaticallyImplyLeading: false. There is no back button, so the page underneath stays alive under it and only system back or tapping the list gets you out.

Suggest switching both to goNamed, like channel_list.dart and the other call sites this PR changed.

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.

Switched both to goNamed in cc1bc92. No pushNamed to the channel page is left.

);

// Read up front: the modal can outlive this message, and with it `context`.
final configuration = StreamChatConfiguration.of(context);

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 fixes the modal's builder, but the action still runs on context after the modal closes. Line 990 calls _onActionTap(context, channel, action).ignore() after await showStreamDialog(...), with no context.mounted check, and that is the context this comment says can be gone.

If the message leaves the tree while the modal is open:

  • React: _selectReaction calls StreamChatConfiguration.of(context) again (line 1089), the lookup this PR moved up front.
  • Delete / Flag: _maybeDeleteMessage and _maybeFlagMessage call showStreamDialog(context: context).

These throw inside the async _onActionTap, and .ignore() discards the error. The modal closes, nothing happens, and nothing is logged.

Suggested fix:

  • Pass the configuration already read here into _selectReaction.
  • Capture the navigator (and translations) before the await, or bail out or log on !context.mounted.
  • Extend stream_message_item_actions_modal_test.dart to tap an action (for example a reaction) after the message is removed.

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.

Went with bailing out on !context.mounted in 40bcf16, after the actions modal, the bounced-message dialog and the reactions sheet. That matches Flutter's PopupMenuButton, which drops the selection when the button is no longer mounted.

The item only leaves the tree while its modal is open when the message is removed from the channel, so the action is dropped rather than run against a message that's gone. The new test taps Thread Reply after the message is removed and expects onThreadTap not to be called. Thread Reply is one of the actions that never touch context, so the test fails without the check.

xsahil03x and others added 3 commits October 16, 2026 13:08
The channel page has a fixed key, so pushing it could stack a duplicate
page or leave one under the chat pane with no way back.

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

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