Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.git
ref: f022a26fea479b97d5a999cb8a7cdf5969b075f7
ref: 66ee511050b8ef4f8a39edf0c2c62568b521d34d
path: packages/stream_core_flutter

dev_dependencies:
Expand Down
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 @@ -132,7 +132,7 @@ void setupMockChannel({
when(() => channel.lastMessageAtStream).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.isDistinct).thenReturn(false);
when(() => channel.isMuted).thenReturn(false);
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.git
ref: f022a26fea479b97d5a999cb8a7cdf5969b075f7
ref: 66ee511050b8ef4f8a39edf0c2c62568b521d34d
path: packages/stream_core_flutter
synchronized: ^3.4.0
thumblr: ^0.0.4
Expand Down
11 changes: 11 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

✅ 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.
- Added `MessageComposerProps.composerLocation` — explicitly controls whether `StreamMessageComposer` renders in `floating` or `docked` mode; falls back to the theme's `appStyle.composerLocation` when null. `copyWith` now distinguishes "not passed" from "explicitly set to null" via a sentinel so callers can clear an override back to the theme default.
- 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 `appBarBehavior` parameter to `StreamChannelHeader`, `StreamChannelListHeader`, and `StreamBackButton`, controlling floating vs pinned app bar appearance (avatar shadow, back-button style). Falls back to `appStyle.appBarBehavior` from the theme when null.
- 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.
- Re-exported `StreamScaffold`, `StreamScaffoldInsets`, `StreamBottomNavBar`, `StreamBottomNavBarItem`, `StreamAppStyle`, `AppBarBehavior`, `BottomBarBehavior`, `ComposerLocation`, and `streamFloatingFade` from `stream_core_flutter` via `package:stream_chat_flutter/stream_chat_flutter.dart`.
- Added support for `@channel`, `@here`, role, and user-group mentions — parsed on incoming messages, rendered as styled tappable spans in message text, and selectable from the composer's `@` autocomplete.
- Added a single `mentionItemBuilder` on `StreamMessageComposer` and `StreamMentionAutocompleteOptions` that receives `StreamMentionItemProps` and covers every mention kind. Customise globally via `streamChatComponentBuilders(mentionItem: ...)` or per-instance via the new constructor parameter. Defaults are rendered by `DefaultStreamMentionItem`. Also added `onMention*Tap` callbacks on `StreamMentionAutocompleteOptions`.
- Added `StreamMessageListView.onMentionTap` and `StreamMessageItem.onMentionTap` — receives a typed `StreamMention` (`StreamUserMention`, `StreamChannelMention`, `StreamHereMention`, `StreamRoleMention`, or `StreamGroupMention`).
Expand All @@ -27,6 +36,8 @@

🐞 Fixed

- Fixed `StreamChannelPage` leaking its `StreamMessageComposerController` — the state now disposes it in `dispose()`, matching the pattern already used by `StreamThreadPage`.
- Fixed `StreamPhotoGallery` default padding having a redundant explicit `top: 0` (no visual change).
- `StreamMessageItem.onUserAvatarTap` now fires when the author avatar is tapped. ([#2741](https://github.com/GetStream/stream-chat-flutter/issues/2741))
- Added `MessageComposerProps.copyWith` so factory overrides can tweak individual props (e.g. `useSystemAttachmentPicker`) without re-specifying every field. ([#2742](https://github.com/GetStream/stream-chat-flutter/issues/2742))
- `StreamMessageContent` no longer re-runs `setState` on every inherited-widget change when the measured attachment width has not changed. ([#2761](https://github.com/GetStream/stream-chat-flutter/issues/2761))
Expand Down
19 changes: 17 additions & 2 deletions packages/stream_chat_flutter/lib/src/channel/channel_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,21 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget
var subtitle = this.subtitle;
subtitle ??= StreamChannelInfo(channel: channel);

final effectiveAppBarBehavior =
style?.behavior ??
StreamAppBarTheme.of(context).style?.behavior ??
(StreamTheme.of(context).appStyle.isFloating ? .floating : .regular);
final showAvatarShadow = switch (effectiveAppBarBehavior) {
.floating => true,
.regular => false,
};

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

return Portal(
child: StreamConnectionStatusBuilder(
Expand Down Expand Up @@ -197,10 +210,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.isFloating = false});

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

@override
Widget build(BuildContext context) {
Expand All @@ -221,6 +235,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
Expand Up @@ -125,7 +125,16 @@ 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 effectiveAppBarBehavior =
style?.behavior ??
StreamAppBarTheme.of(context).style?.behavior ??
(StreamTheme.of(context).appStyle.isFloating ? .floating : .regular);
final hasAvatarShadow = switch (effectiveAppBarBehavior) {
.floating => true,
.regular => false,
};

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

return Portal(
child: StreamConnectionStatusBuilder(
Expand Down Expand Up @@ -179,10 +188,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.isFloating = false});

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

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -211,6 +221,7 @@ class _DefaultUserAvatar extends StatelessWidget {
size: .lg,
user: user,
showOnlineIndicator: false,
isFloating: isFloating,
),
),
),
Expand Down
148 changes: 148 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,148 @@
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;
late final StreamMessageComposerController _messageComposerController;

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

@override
void dispose() {
_focusNode.dispose();
_messageComposerController.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