Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
c9e00d4
Create ChannelPage with floating logic
renefloor May 6, 2026
9e5ba93
simplify composer setup
renefloor May 6, 2026
e6e3444
poc for floating app bar
renefloor May 6, 2026
c99bc63
fixes for merge issues
renefloor May 27, 2026
905b868
Revert "simplify composer setup"
renefloor May 27, 2026
bb28a92
fix avatar press
renefloor May 27, 2026
c6b4840
fix composer safeArea
renefloor May 27, 2026
413e987
Add floating header
renefloor May 27, 2026
66af712
Cleanup sample app
renefloor May 27, 2026
2c156b2
Fix topPadding on listview
renefloor May 27, 2026
d42cc51
Fix attachment picker padding
renefloor May 27, 2026
68f1ed0
Using theming for floating config
renefloor May 28, 2026
aac36c3
improve using default floatings from theme
renefloor May 28, 2026
558f073
Use streamscaffold
renefloor May 29, 2026
1ff997d
migrate to SDK navbar
renefloor May 29, 2026
4bd8056
fix edit message in thread
renefloor Jun 10, 2026
84e85ce
Add shadow to avatar on channel header
renefloor Jun 11, 2026
c27569b
Add gradient behind floating composer
renefloor Jun 12, 2026
4b0a700
fix attachmentpicker background
renefloor Jun 12, 2026
cee1b1d
Add sample app config
renefloor Jun 12, 2026
9ba1ba8
Fix extra padding on loading skeletons
renefloor Jun 12, 2026
dfedb96
Update all sample app pages
renefloor Jun 12, 2026
c431a3b
update core dependency
renefloor Jun 12, 2026
fb4f234
Add global message list config
renefloor Jun 12, 2026
3ac8eb5
Fix scroll button showing on thread
renefloor Jun 12, 2026
cf907e7
Check if thread replies are enabled
renefloor Jun 12, 2026
3e477cc
fix background attachment picker
renefloor Jun 12, 2026
74888d1
set default appstyle to regular
renefloor Jun 12, 2026
a67a716
fix review comments
renefloor Jun 12, 2026
d1ad9e1
update core dependency
renefloor Jun 16, 2026
c44ba13
update changelog
renefloor Jun 16, 2026
0c62f89
update core dependency
renefloor Jun 17, 2026
738d676
fix(ui): channel page controller leak, shared fade helper, photo gall…
renefloor Jun 17, 2026
e966ad3
Update appstyle theming
renefloor Jun 17, 2026
faf7c8a
update core dependency
renefloor Jun 17, 2026
01a5a71
Merge remote-tracking branch 'origin/master' into feat/channel-page
renefloor Jun 19, 2026
38a0c59
update core dependency
renefloor Jun 19, 2026
26d0f3f
chore: Update Goldens
renefloor Jun 19, 2026
5730ff6
update core and fix analysis
renefloor Jun 19, 2026
6b1db2a
add option for reply in channel config
renefloor Jun 19, 2026
6593cc7
chore: Update Goldens
renefloor Jun 19, 2026
edd53f2
fix not using new api
renefloor Jun 19, 2026
414fd1b
fix test and analysis
renefloor Jun 19, 2026
017e9c4
Merge remote-tracking branch 'origin/master' into feat/channel-page
renefloor Jun 19, 2026
3eb63d5
Merge remote-tracking branch 'origin/master' into feat/channel-page
renefloor Jul 29, 2026
1862ec0
project update
renefloor Jul 29, 2026
4490030
fix pixel artifact between components
renefloor Jul 29, 2026
00ee06e
change background into 1 gradient
renefloor Jul 29, 2026
2fc96e3
Add fixes for channelpage and deleted message
renefloor Jul 29, 2026
a9ad64c
Add snapshot tests for deleted message
renefloor Jul 29, 2026
f7ae533
chore: Update Goldens
renefloor Jul 29, 2026
a54b59e
Add floating to edit button group page
renefloor Jul 29, 2026
0dd9add
use elevation for composer
renefloor Jul 30, 2026
d694d88
Add composer background animation
renefloor Jul 30, 2026
775efe0
update core dependency
renefloor Jul 30, 2026
a0ea0e6
Merge remote-tracking branch 'origin/master' into feat/channel-page
renefloor Jul 30, 2026
69df35a
update core dependency
renefloor Jul 30, 2026
d310daa
fix change in elevation logic
renefloor Jul 30, 2026
989e58e
Merge remote-tracking branch 'origin/master' into feat/channel-page
renefloor Jul 30, 2026
95cf893
fix(sample): restore location sharing in the channel page
renefloor Jul 30, 2026
e240ac2
docs(ui): document the channel/thread page extension points
renefloor Jul 30, 2026
557b9ab
docs(changelog): correct the appBarBehavior entry
renefloor Jul 30, 2026
14ed0a6
docs(changelog): note the thread scroll-to-bottom fix
renefloor Jul 30, 2026
ad9e6a6
fix(ui): resolve message list config on the build path
renefloor Jul 30, 2026
33eeab5
test(ui): cover replies-disabled hiding the thread reply action
renefloor Jul 30, 2026
e97ab7f
refactor(ui): extract the app bar behavior resolver
renefloor Jul 30, 2026
26ea9ff
chore: minor cleanups from PR review
renefloor Jul 30, 2026
d63dde3
Merge remote-tracking branch 'origin/master' into feat/channel-page
renefloor Jul 30, 2026
af5ec1e
move custom composer
renefloor Jul 30, 2026
1a156e4
formatting
renefloor Jul 30, 2026
42bfd56
minor improvements
renefloor Jul 30, 2026
e3e50b8
improvements on custom attachment picker
renefloor Jul 30, 2026
4cf55e1
Merge branch 'fix/empty-unread-indicator' into feat/channel-page
renefloor Jul 31, 2026
bd3c0a2
fix test mocks
renefloor Jul 31, 2026
da535fb
Simplify back buttons in new pages
renefloor Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs_screenshots/test/src/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ void setupMockChannel({
).thenAnswer((_) => Stream.value(DateTime.parse('2020-06-22 12:00:00')));
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
when(() => channel.config).thenReturn(ChannelConfig(mutes: true));
when(() => channel.config).thenReturn(ChannelConfig(mutes: true, replies: true));
when(channel.getRemainingCooldown).thenReturn(0);
when(() => channel.getRemainingCooldown(lastMessageAt: any(named: 'lastMessageAt'))).thenReturn(0);
when(() => channel.isDistinct).thenReturn(false);
Expand Down
Binary file modified docs/docs_screenshots/test/theming/goldens/macos/theming_red.png

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.

This is from the HSL -> HCT color change

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
## Upcoming

⚠️ Changed

- `StreamPhotoGallery` now defaults its `padding` to the bottom safe-area inset instead of no padding, so the last row clears the home indicator. Pass an explicit `padding` to opt out.
- `StreamMessageListView.config` is now nullable (`StreamMessageListViewConfiguration?`) instead of defaulting to `const StreamMessageListViewConfiguration()`. Passing a value is unchanged; code that *reads* `config` off a `StreamMessageListView` instance now gets a nullable value.

✅ Added

- Added `StreamChannelPage` — a ready-to-use channel page widget that wires up `StreamChannelHeader`, `StreamMessageListView`, and `StreamMessageComposer` with floating or docked layout driven by the active app style.
- Added `StreamThreadPage` — a ready-to-use thread page widget with the same floating/docked layout support, plus an `onBackPressed` callback that replaces the header back button's default pop.
- Added `onBackPressed` to `StreamChannelHeader` and `StreamThreadHeader` — replaces the default back button's `Navigator.maybePop` without rebuilding the whole `leading`, so callers keep the header's default unread badge. Ignored when `leading` is supplied.
- Added `StreamComposerLocation` and `MessageComposerProps.location` — explicitly controls whether `StreamMessageComposer` renders `floating` or `docked`. When null it falls back to `StreamMessageComposerThemeData.location`, and then to the ambient `StreamAppStyle`.
- Added `StreamMessageComposerTheme` and `StreamMessageComposerThemeData` — a component theme for the composer, also available globally as `StreamChatThemeData.messageComposerTheme`.
- Added `StreamMessageListView.config` — accepts an explicit `StreamMessageListViewConfiguration` per widget; falls back to `StreamChatConfigurationData.messageListViewConfiguration` from the nearest ancestor when omitted.
- Added `StreamMessageListView.topPadding` and `bottomPadding` — padding applied to the scroll view's top and bottom edges, used by floating app bar / composer layouts to keep messages visible without an extra `setState`.
- Added an `appBarBehavior` parameter to `StreamBackButton`, controlling floating vs pinned back-button appearance. Falls back to `StreamAppBarTheme`, and then to the ambient `StreamAppStyle`, when null. `StreamChannelHeader`, `StreamChannelListHeader`, and `StreamThreadHeader` now merge their `style` into the `StreamAppBarTheme` they install, so every slot in the bar — the default avatar's drop shadow, the default back button, and any `leading`/`trailing` the caller supplies — resolves the same chain the bar itself uses: `style`, then the per-header theme (`channelHeaderTheme` / `channelListHeaderTheme` / `threadHeaderTheme`), then `StreamAppBarTheme`, then `StreamAppStyle`.
- Added `messageListViewConfiguration` field to `StreamChatConfigurationData`, allowing a global `StreamMessageListViewConfiguration` default for all `StreamMessageListView` widgets. Pass it via `StreamChat.configData` to configure behaviors like `swipeToReply` and `highlightInitialMessage` app-wide without wiring them per-page.
- Re-exported `StreamScaffold`, `StreamScaffoldInsets`, `StreamBottomNavBar`, `StreamBottomNavBarItem`, `StreamAppStyle`, `StreamAppBarBehavior`, `StreamBottomAppBarBehavior`, and `streamFloatingFadeLinearGradient` from `stream_core_flutter` via `package:stream_chat_flutter/stream_chat_flutter.dart`.
- Added `StreamMessageListViewConfiguration.autoScrollPolicy` to control whether and how `StreamMessageListView` scrolls to the newest message when a new message arrives. Use `StreamAutoScrollPolicy.disabled` to fully control scrolling yourself.
- Added `onReactionSelected` to `StreamMessageReactionPicker`, a context-aware callback that provides the `BuildContext` for navigation.
- Added an `errorSubtitle` to `StreamScrollViewErrorWidget`, which now falls back to the design's generic error copy (title, description, and a "Try Again" retry label) when values aren't provided.
Expand All @@ -22,6 +37,8 @@
- Fixed the default list/scroll-view error states (channel, message, member, user, thread, poll-vote, reaction, search, and photo) showing raw or fixed errors; they are now connection-aware (no internet / slow connection), falling back to each view's specific error text.
- Fixed `StreamTypingIndicator` briefly showing typing users from a different context (main channel vs. thread) on its first frame.
- Fixed the attachment picker throwing a `Tooltip` assertion error when a custom `TabbedAttachmentPickerOption` is added without a `title`; the tooltip is now only shown when a title is provided.
- Fixed the "Message deleted" bubble overflowing its maximum width when the localized label is long; the label now wraps instead.
- Fixed the scroll-to-bottom button in thread views keying off the parent channel's up-to-date state instead of the thread's own scroll position, so it no longer appears while already at the newest reply. Note that thread views now show the button at all: it follows `showScrollToBottom` from the effective `StreamMessageListViewConfiguration`, so set that to `false` to restore the previous always-hidden behavior.
- Fixed the `StreamBackButton` unread badge including the currently open channel in its total count.

## 10.2.0
Expand Down
26 changes: 23 additions & 3 deletions packages/stream_chat_flutter/lib/src/channel/channel_header.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter/src/utils/app_bar_behavior.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// {@template streamChannelHeader}
Expand Down Expand Up @@ -76,6 +77,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
this.showConnectionStateTile = false,
this.leading,
this.automaticallyImplyLeading = true,
this.onBackPressed,
this.title,
this.subtitle,
this.trailing,
Expand Down Expand Up @@ -104,6 +106,12 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
/// Defaults to `true`. Set to `false` to suppress the back button.
final bool automaticallyImplyLeading;

/// Called when the default [StreamBackButton] is pressed, replacing its
/// default `Navigator.maybePop`.
///
/// Ignored when [leading] is provided.
final VoidCallback? onBackPressed;

/// {@macro StreamAppBar.title}
///
/// Defaults to a [StreamChannelName] for the enclosing channel.
Expand Down Expand Up @@ -140,6 +148,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
var leading = this.leading;
if (leading == null && automaticallyImplyLeading) {
leading = StreamBackButton(
onPressed: onBackPressed,
unreadIndicator: StreamUnreadIndicator(excludeCid: channel.cid),
);
}
Expand Down Expand Up @@ -177,10 +186,15 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
message: statusString,
// Wrap the bar in a [StreamAppBarTheme] so the per-header chat
// theme drives all default styling (background, padding,
// typography, divider) — the bar internally merges in any
// [style] override the caller passed.
// typography, divider).
//
// [style] is merged into the theme data as well as passed to the
// bar, so every slot below — the default avatar, the default back
// button, and anything the caller supplied — resolves the same
// chain the bar itself does: [style], then the per-header theme,
// then the ambient app bar theme, then the app style.
child: StreamAppBarTheme(
data: headerTheme,
data: headerTheme.merge(StreamAppBarThemeData(style: style)),
child: StreamAppBar(
leading: leading,
automaticallyImplyLeading: false,
Expand All @@ -207,6 +221,11 @@ class _DefaultChannelAvatar extends StatelessWidget {

@override
Widget build(BuildContext context) {
// Resolved from context rather than passed in, so it picks up the header's
// [StreamAppBarTheme] — this widget is built inside it, same as the bar's
// other slots.
final isFloating = isFloatingAppBar(context);

final effectiveOnTap = switch (onPressed) {
final cb? => () => cb(channel),
_ => null,
Expand All @@ -224,6 +243,7 @@ class _DefaultChannelAvatar extends StatelessWidget {
child: StreamChannelAvatar(
size: .lg,
channel: channel,
isFloating: isFloating,
),
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter/src/utils/app_bar_behavior.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// {@template streamChannelListHeader}
Expand Down Expand Up @@ -158,10 +159,15 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi
showMessage: showConnectionStateTile && showStatus,
message: statusString,
// Wrap the bar in a [StreamAppBarTheme] so the per-header chat
// theme drives all default styling — the bar internally merges
// in any [style] override the caller passed.
// theme drives all default styling.
//
// [style] is merged into the theme data as well as passed to the
// bar, so every slot below — the default avatar and anything the
// caller supplied — resolves the same chain the bar itself does:
// [style], then the per-header theme, then the ambient app bar
// theme, then the app style.
child: StreamAppBarTheme(
data: headerTheme,
data: headerTheme.merge(StreamAppBarThemeData(style: style)),
child: StreamAppBar(
leading: leading,
title: title,
Expand Down Expand Up @@ -189,6 +195,11 @@ class _DefaultUserAvatar extends StatelessWidget {
final user = client.state.currentUser;
if (user == null) return const SizedBox.shrink();

// Resolved from context rather than passed in, so it picks up the header's
// [StreamAppBarTheme] — this widget is built inside it, same as the bar's
// other slots.
final isFloating = isFloatingAppBar(context);

// Caller-provided handler wins; otherwise mirror Material AppBar and
// open the enclosing Scaffold's drawer if one exists. With no callback
// and no drawer, the avatar is non-interactive.
Expand All @@ -211,6 +222,7 @@ class _DefaultUserAvatar extends StatelessWidget {
size: .lg,
user: user,
showOnlineIndicator: false,
isFloating: isFloating,
),
),
),
Expand Down
200 changes: 200 additions & 0 deletions packages/stream_chat_flutter/lib/src/channel/channel_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// A channel page with optional floating composer support.
///
/// Wires up a [StreamChannelHeader], a [StreamMessageListView] and a
/// [StreamMessageComposer], laid out floating or docked according to the ambient
/// [StreamAppStyle]. Expects a [StreamChannel] ancestor.
///
/// ## Customizing this page
///
/// The constructor is deliberately small — most customization happens through
/// the component factory and the global configuration, both of which reach
/// inside this page because the components resolve them themselves.
///
/// Swap out components with [streamChatComponentBuilders], passed to
/// [StreamChat.componentBuilders]:
///
/// ```dart
/// StreamChat(
/// client: client,
/// componentBuilders: StreamComponentBuilders(
/// extensions: streamChatComponentBuilders(
/// // Applies to the messages this page's list renders.
/// messageItem: (context, props) => DefaultStreamMessageItem(
/// props: props.copyWith(maxWidth: 320),
/// ),
/// // Applies to this page's composer.
/// messageComposer: (context, props) => DefaultStreamMessageComposer(
/// props: props.copyWith(disableAttachments: true),
/// ),
/// ),
/// ),
/// child: child,
/// )
/// ```
///
/// `messageItem`, `messageComposer`, `quotedMessage`, `mentionItem`, the
/// attachment builders, `mediaGallery` and `videoPlayer` all apply here.
///
/// Change list behaviour — `swipeToReply`, `highlightInitialMessage`,
/// `autoScrollPolicy` and the rest — through
/// [StreamChatConfigurationData.messageListViewConfiguration] on
/// [StreamChat.configData].
///
/// Not reachable from here: the list-level slots on
/// [StreamMessageListViewBuilders] (`header`, `footer`, `dateDivider`,
/// `floatingDateDivider`, `threadSeparator`, `scrollToBottomButton`, `empty`,
/// `loading`, `error`) and [StreamChannelHeader]'s title, subtitle and actions.
/// Those have no component-factory entry, so customizing them means composing
/// [StreamMessageListView] and [StreamChannelHeader] directly instead of using
/// this page.
///
/// See also:
///
/// * [StreamThreadPage], the equivalent page for a single thread.
class StreamChannelPage extends StatefulWidget {
/// Creates a [StreamChannelPage].
const StreamChannelPage({
super.key,
this.initialScrollIndex,
this.initialAlignment,
this.onBackPressed,
this.onChannelAvatarPressed,
});

/// Initial scroll index for the message list.
final int? initialScrollIndex;

/// Initial scroll alignment for the message list.
final double? initialAlignment;

/// Called when the header's back button is pressed.
///
/// Replaces the default action, which pops the current route. When null the
/// default is kept.
final VoidCallback? onBackPressed;

/// Called when the default channel-avatar trailing is pressed.
final void Function(BuildContext context, Channel channel)? onChannelAvatarPressed;

@override
State<StreamChannelPage> createState() => _StreamChannelPageState();
}

class _StreamChannelPageState extends State<StreamChannelPage> {
late final FocusNode _focusNode = FocusNode();
late final StreamMessageComposerController _messageComposerController = StreamMessageComposerController();

@override
void dispose() {
_focusNode.dispose();
_messageComposerController.dispose();
super.dispose();
}

void _reply(Message message) {
_messageComposerController.quotedMessage = message;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_focusNode.requestFocus();
});
}

void _editMessage(Message message) {
_messageComposerController.editMessage(message);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_focusNode.requestFocus();
});
}

@override
Widget build(BuildContext context) {
final appBar = StreamChannelHeader(
// Leaving this null keeps the header's default back button, which pops
// the route.
onBackPressed: widget.onBackPressed,
onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel),
);

final composer = StreamMessageComposer(
focusNode: _focusNode,
messageComposerController: _messageComposerController,
onQuotedMessageCleared: _messageComposerController.clearQuotedMessage,
enableVoiceRecording: true,
);

final typingIndicator = StreamTypingIndicator(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
style: context.streamTextTheme.captionDefault.copyWith(
color: context.streamColorScheme.textSecondary,
),
);

return StreamScaffold(
appBar: appBar,
bottom: composer,
body: _ChannelPageBody(
initialScrollIndex: widget.initialScrollIndex,
initialAlignment: widget.initialAlignment,
onReply: _reply,
onEditMessage: _editMessage,
typingIndicator: typingIndicator,
),
);
}
}

/// The body of [StreamChannelPage].
///
/// Reads [StreamScaffoldInsets] to provide correct [topPadding] and
/// [bottomPadding] to [StreamMessageListView], and positions the typing
/// indicator just above the composer (floating or docked) using the same
/// inset values.
class _ChannelPageBody extends StatelessWidget {
const _ChannelPageBody({
required this.typingIndicator,
required this.onReply,
required this.onEditMessage,
this.initialScrollIndex,
this.initialAlignment,
});

final Widget typingIndicator;
final void Function(Message) onReply;
final void Function(Message) onEditMessage;
final int? initialScrollIndex;
final double? initialAlignment;

@override
Widget build(BuildContext context) {
final insets = StreamScaffoldInsets.of(context);

return Stack(
children: [
StreamMessageListView(
initialScrollIndex: initialScrollIndex,
initialAlignment: initialAlignment,
onEditMessageTap: onEditMessage,
onReplyTap: onReply,
threadBuilder: (_, parentMessage) {
return StreamThreadPage(parent: parentMessage!);
},
topPadding: insets.topPadding,
bottomPadding: insets.bottomPadding,
),
Positioned(
bottom: insets.bottomPadding,
left: 0,
right: 0,
child: typingIndicator,
),
],
);
}
}
Loading
Loading