Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
44 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
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/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies:
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter
ref: 43f90a46d8092ca2c1e24c36af06f05fc0c633a6
ref: 6f8ba0c07a6ffbc6e0ea96c560f010cf48b8012b
path: packages/stream_core_flutter

dev_dependencies:
Expand Down
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ command:
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter
ref: 43f90a46d8092ca2c1e24c36af06f05fc0c633a6
ref: 6f8ba0c07a6ffbc6e0ea96c560f010cf48b8012b
path: packages/stream_core_flutter
synchronized: ^3.4.0
thumblr: ^0.0.4
Expand Down
1 change: 1 addition & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

✅ Added

- 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.
- `StreamMessageComposer` now surfaces the hold-to-record hint through `StreamSnackbar` anchored above the composer, and `StreamChat` provides an app-wide `StreamSnackbarScope` fallback.

⚠️ Deprecated
Expand Down
24 changes: 21 additions & 3 deletions packages/stream_chat_flutter/lib/src/channel/channel_header.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_core_flutter/stream_core_flutter.dart';

/// {@template streamChannelHeader}
/// A top-of-screen header for a single channel.
Expand Down Expand Up @@ -81,6 +82,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
this.trailing,
this.primary = true,
this.style,
this.appBarBehavior,
});

/// Called when the default channel-avatar trailing is pressed.
Expand Down Expand Up @@ -129,6 +131,10 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
/// [StreamChatThemeData.channelHeaderTheme].
final StreamAppBarStyle? style;

/// Controls the header's visual/layout behavior (e.g. floating vs pinned).
/// Falls back to the theme's default when null.
final AppBarBehavior? appBarBehavior;

@override
Size get preferredSize => const Size.fromHeight(kStreamToolbarHeight);

Expand All @@ -139,7 +145,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget

var leading = this.leading;
if (leading == null && automaticallyImplyLeading) {
leading = const StreamBackButton(showUnreadCount: true);
leading = StreamBackButton(showUnreadCount: true, appBarBehavior: appBarBehavior);
}

var title = this.title;
Expand All @@ -148,8 +154,17 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
var subtitle = this.subtitle;
subtitle ??= StreamChannelInfo(channel: channel);

final showAvatarShadow = switch (appBarBehavior ?? StreamTheme.of(context).appStyle.appBarBehavior) {
.floating => true,
.regular => false,
};

var trailing = this.trailing;
trailing ??= _DefaultChannelAvatar(channel: channel, onPressed: onChannelAvatarPressed);
trailing ??= _DefaultChannelAvatar(
channel: channel,
onPressed: onChannelAvatarPressed,
showShadow: showAvatarShadow,
);

return Portal(
child: StreamConnectionStatusBuilder(
Expand Down Expand Up @@ -187,6 +202,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
trailing: trailing,
primary: primary,
style: style,
appBarBehavior: appBarBehavior,
),
),
);
Expand All @@ -197,10 +213,11 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
}

class _DefaultChannelAvatar extends StatelessWidget {
const _DefaultChannelAvatar({required this.channel, this.onPressed});
const _DefaultChannelAvatar({required this.channel, this.onPressed, this.showShadow = false});

final Channel channel;
final void Function(Channel channel)? onPressed;
final bool showShadow;

@override
Widget build(BuildContext context) {
Expand All @@ -221,6 +238,7 @@ class _DefaultChannelAvatar extends StatelessWidget {
child: StreamChannelAvatar(
size: .lg,
channel: channel,
showShadow: showShadow,
),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi
this.trailing,
this.primary = true,
this.style,
this.appBarBehavior,
});

/// Use this if you don't have a [StreamChatClient] in your widget tree.
Expand Down Expand Up @@ -117,6 +118,10 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi
/// [StreamChatThemeData.channelListHeaderTheme].
final StreamAppBarStyle? style;

/// Controls the header's visual/layout behavior (e.g. floating vs pinned).
/// Falls back to the theme's default when null.
final AppBarBehavior? appBarBehavior;

@override
Size get preferredSize => const Size.fromHeight(kStreamToolbarHeight);

Expand All @@ -125,7 +130,12 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi
final _client = client ?? StreamChat.of(context).client;
final headerTheme = StreamChatTheme.of(context).channelListHeaderTheme;

final leading = _DefaultUserAvatar(client: _client, onPressed: onUserAvatarPressed);
final hasAvatarShadow = switch (appBarBehavior ?? StreamTheme.of(context).appStyle.appBarBehavior) {
.floating => true,
.regular => false,
};

final leading = _DefaultUserAvatar(client: _client, onPressed: onUserAvatarPressed, showShadow: hasAvatarShadow);

return Portal(
child: StreamConnectionStatusBuilder(
Expand Down Expand Up @@ -179,10 +189,11 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi
}

class _DefaultUserAvatar extends StatelessWidget {
const _DefaultUserAvatar({required this.client, this.onPressed});
const _DefaultUserAvatar({required this.client, this.onPressed, this.showShadow = false});

final StreamChatClient client;
final void Function(User user)? onPressed;
final bool showShadow;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -211,6 +222,7 @@ class _DefaultUserAvatar extends StatelessWidget {
size: .lg,
user: user,
showOnlineIndicator: false,
showShadow: showShadow,
),
),
),
Expand Down
146 changes: 146 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,146 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// A channel page with optional floating composer support.
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;

/// Callback for when the back button is pressed.
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;
final _messageComposerController = StreamMessageComposerController();

@override
void initState() {
_focusNode = FocusNode();
super.initState();
}

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

void _reply(Message message) {
_messageComposerController.quotedMessage = message;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus();
});
}

void _editMessage(Message message) {
_messageComposerController.editMessage(message);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus();
});
}

@override
Widget build(BuildContext context) {
final appBar = StreamChannelHeader(
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(
backgroundColor: context.streamColorScheme.backgroundApp,
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