From c9e00d422e585735811010b80b25a747c2acaaaa Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 6 May 2026 14:56:13 +0200 Subject: [PATCH 01/48] Create ChannelPage with floating logic --- .../lib/src/channel/channel_page.dart | 271 ++++++++++++++++++ .../lib/src/channel/thread_page.dart | 85 ++++++ .../stream_message_composer.dart | 49 ++-- .../message_list_view/message_list_view.dart | 14 +- .../lib/stream_chat_flutter.dart | 1 + sample_app/lib/routes/app_routes.dart | 3 +- 6 files changed, 401 insertions(+), 22 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/channel/channel_page.dart create mode 100644 packages/stream_chat_flutter/lib/src/channel/thread_page.dart diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart new file mode 100644 index 0000000000..5b4ee980ab --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -0,0 +1,271 @@ +// ignore_for_file: deprecated_member_use, avoid_redundant_argument_values + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/channel/thread_page.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.highlightInitialMessage = false, + this.onBackPressed, + this.onHeaderImageTap, + this.isFloating = false, + }); + + /// Initial scroll index for the message list. + final int? initialScrollIndex; + + /// Initial scroll alignment for the message list. + final double? initialAlignment; + + /// Whether to highlight the initial message. + final bool highlightInitialMessage; + + /// Callback for when the back button is pressed. + final VoidCallback? onBackPressed; + + /// Callback for when the header image is tapped. + final VoidCallback? onHeaderImageTap; + + /// Whether the composer floats over the message list. + /// + /// When true the composer is overlaid at the bottom and the message list + /// scrolls underneath it. Layout is done in a single pass so the list + /// inset and the composer height are always in sync. + final bool isFloating; + + @override + State createState() => _StreamChannelPageState(); +} + +class _StreamChannelPageState extends State { + FocusNode? _focusNode; + final _messageInputController = StreamMessageInputController(); + + @override + void initState() { + _focusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + _focusNode!.dispose(); + super.dispose(); + } + + void _reply(Message message) { + _messageInputController.quotedMessage = message; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _focusNode!.requestFocus(); + }); + } + + void _editMessage(Message message) { + _messageInputController.editMessage(message); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _focusNode!.requestFocus(); + }); + } + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final textTheme = theme.textTheme; + final colorTheme = theme.colorTheme; + + final appBar = StreamChannelHeader( + showTypingIndicator: false, + onBackPressed: widget.onBackPressed, + onImageTap: widget.onHeaderImageTap, + ); + + final typingIndicator = StreamTypingIndicator( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + style: textTheme.footnote.copyWith( + color: colorTheme.textLowEmphasis, + ), + ); + + final composer = StreamMessageComposer( + focusNode: _focusNode, + messageInputController: _messageInputController, + onQuotedMessageCleared: _messageInputController.clearQuotedMessage, + enableVoiceRecording: true, + isFloating: widget.isFloating, + ); + + if (widget.isFloating) { + return Scaffold( + backgroundColor: colorTheme.appBg, + appBar: appBar, + body: _FloatingChannelBody( + composer: composer, + typingIndicator: Container( + alignment: Alignment.centerLeft, + color: colorTheme.appBg.withOpacity(.9), + child: typingIndicator, + ), + messageListBuilder: (bottomWidgetsHeight) => StreamMessageListView( + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + highlightInitialMessage: widget.highlightInitialMessage, + onEditMessageTap: _editMessage, + onReplyTap: _reply, + swipeToReply: true, + threadBuilder: (_, parentMessage) { + return StreamThreadPage(parent: parentMessage!); + }, + bottomPadding: bottomWidgetsHeight, + ), + ), + ); + } + + return Scaffold( + backgroundColor: colorTheme.appBg, + appBar: appBar, + body: Column( + children: [ + Expanded( + child: Stack( + children: [ + StreamMessageListView( + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + highlightInitialMessage: widget.highlightInitialMessage, + onEditMessageTap: _editMessage, + onReplyTap: _reply, + swipeToReply: true, + threadBuilder: (_, parentMessage) { + return StreamThreadPage(parent: parentMessage!); + }, + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + alignment: Alignment.centerLeft, + color: colorTheme.appBg.withOpacity(.9), + child: typingIndicator, + ), + ), + ], + ), + ), + composer, + ], + ), + ); + } +} + +// --------------------------------------------------------------------------- +// Floating layout helpers +// --------------------------------------------------------------------------- + +/// Layout slot identifiers for [_FloatingChannelBodyDelegate]. +enum _Slot { body, typing, composer } + +/// A custom [BoxConstraints] subclass that carries the composer height so that +/// a [LayoutBuilder] inside the body slot can read it without a separate +/// callback or [setState]. +/// +/// The [==] / [hashCode] overrides are intentional: they make the framework +/// re-run [RenderObject.performLayout] on the body child even when the outer +/// size constraints have not changed but only [composerHeight] has. +class _BodyBoxConstraints extends BoxConstraints { + const _BodyBoxConstraints({ + super.maxWidth, + super.maxHeight, + required this.composerHeight, + }) : assert(composerHeight >= 0, 'composerHeight must be non-negative'); + + /// Height of the floating composer, used as the list's bottom inset. + final double composerHeight; + + @override + bool operator ==(Object other) { + if (super != other) return false; + return other is _BodyBoxConstraints && other.composerHeight == composerHeight; + } + + @override + int get hashCode => Object.hash(super.hashCode, composerHeight); +} + +/// [MultiChildLayoutDelegate] that measures the composer first, then lays out +/// the typing indicator and body in the same layout pass — no post-frame +/// callback required. +class _FloatingChannelBodyDelegate extends MultiChildLayoutDelegate { + @override + void performLayout(Size size) { + // 1. Measure + position the composer at the bottom. + final composerSize = layoutChild(_Slot.composer, BoxConstraints.loose(size)); + positionChild(_Slot.composer, Offset(0, size.height - composerSize.height)); + + // 2. Measure + position the typing indicator just above the composer. + final typingSize = layoutChild(_Slot.typing, BoxConstraints(maxWidth: size.width)); + positionChild(_Slot.typing, Offset(0, size.height - composerSize.height - typingSize.height)); + + // 3. Lay out the body with the composer height embedded in the constraints + // so the LayoutBuilder inside can read it synchronously. + layoutChild( + _Slot.body, + _BodyBoxConstraints( + maxWidth: size.width, + maxHeight: size.height, + composerHeight: composerSize.height, + ), + ); + positionChild(_Slot.body, Offset.zero); + } + + @override + bool shouldRelayout(_FloatingChannelBodyDelegate oldDelegate) => false; +} + +/// Stateless widget that wires the floating layout together. +class _FloatingChannelBody extends StatelessWidget { + const _FloatingChannelBody({ + required this.composer, + required this.typingIndicator, + required this.messageListBuilder, + }); + + final Widget composer; + final Widget typingIndicator; + + /// Called with the current composer height so the list can set its inset. + final Widget Function(double bottomWidgetsHeight) messageListBuilder; + + @override + Widget build(BuildContext context) { + return CustomMultiChildLayout( + delegate: _FloatingChannelBodyDelegate(), + children: [ + // Body slot: a LayoutBuilder that reads the custom constraints. + LayoutId( + id: _Slot.body, + child: LayoutBuilder( + builder: (context, constraints) { + final height = constraints is _BodyBoxConstraints ? constraints.composerHeight : 0.0; + return messageListBuilder(height); + }, + ), + ), + LayoutId(id: _Slot.typing, child: typingIndicator), + LayoutId(id: _Slot.composer, child: composer), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart new file mode 100644 index 0000000000..c6797a901f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A page that displays a thread of messages for a given parent message. +class StreamThreadPage extends StatefulWidget { + /// Creates a [StreamThreadPage]. + const StreamThreadPage({ + super.key, + required this.parent, + this.initialScrollIndex, + this.initialAlignment, + this.onViewInChannelTap, + }); + + /// The parent message of the thread. + final Message parent; + + /// Initial scroll index for the thread message list. + final int? initialScrollIndex; + + /// Initial scroll alignment for the thread message list. + final double? initialAlignment; + + /// Called when the user taps "View in channel". + final void Function(Message message)? onViewInChannelTap; + + @override + State createState() => _StreamThreadPageState(); +} + +class _StreamThreadPageState extends State { + final FocusNode _focusNode = FocusNode(); + late StreamMessageInputController _messageInputController; + + @override + void initState() { + super.initState(); + _messageInputController = StreamMessageInputController( + message: Message(parentId: widget.parent.id), + ); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + void _reply(Message message) { + _messageInputController.quotedMessage = message; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _focusNode.requestFocus(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, + appBar: StreamThreadHeader(parent: widget.parent), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + parentMessage: widget.parent, + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + onReplyTap: _reply, + swipeToReply: true, + showScrollToBottom: false, + highlightInitialMessage: true, + onViewInChannelTap: widget.onViewInChannelTap, + ), + ), + if (widget.parent.type != 'deleted') + StreamMessageComposer( + focusNode: _focusNode, + messageInputController: _messageInputController, + enableVoiceRecording: true, + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 565d63b10c..c59327dcb9 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -95,6 +95,7 @@ class StreamMessageComposer extends StatelessWidget { TextCapitalization textCapitalization = TextCapitalization.sentences, bool autofocus = false, bool autoCorrect = true, + bool isFloating = false, }) : props = .new( onMessageSent: onMessageSent, preMessageSending: preMessageSending, @@ -130,6 +131,7 @@ class StreamMessageComposer extends StatelessWidget { textCapitalization: textCapitalization, autofocus: autofocus, autoCorrect: autoCorrect, + isFloating: isFloating, ); /// Creates a [StreamMessageComposer] from a pre-built [MessageComposerProps]. @@ -190,6 +192,7 @@ class MessageComposerProps { this.textCapitalization = TextCapitalization.sentences, this.autofocus = false, this.autoCorrect = true, + this.isFloating = false, }); /// Function called after sending the message. @@ -384,6 +387,8 @@ class MessageComposerProps { /// Defaults to true. final bool autoCorrect; + /// Whether the message composer is floating. + final bool isFloating; /// Returns a copy of this [MessageComposerProps] with the given fields /// replaced with new values. MessageComposerProps copyWith({ @@ -789,25 +794,30 @@ class DefaultStreamMessageComposerState extends State _StreamMessageListViewState(); } @@ -593,7 +600,10 @@ class _StreamMessageListViewState extends State { }, child: ScrollablePositionedList.separated( key: Key('mlv-${streamChannel?.channel.cid}-${widget.parentMessage?.id}'), - padding: .symmetric(vertical: context.streamSpacing.sm), + padding: .only( + top: context.streamSpacing.sm, + bottom: max(widget.bottomPadding, context.streamSpacing.sm), + ), keyboardDismissBehavior: widget.config.keyboardDismissBehavior, itemPositionsListener: _itemPositionListener, initialScrollIndex: initialIndex, @@ -1040,7 +1050,7 @@ class _StreamMessageListViewState extends State { } return PositionedDirectional( - bottom: 16, + bottom: max(16, widget.bottomPadding), end: 16, child: button, ); diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index a434f1a6e5..de2b6ac64d 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -168,6 +168,7 @@ export 'src/channel/channel_header.dart'; export 'src/channel/channel_info.dart'; export 'src/channel/channel_list_header.dart'; export 'src/channel/channel_name.dart'; +export 'src/channel/channel_page.dart'; export 'src/channel/stream_channel_name.dart'; export 'src/channel/stream_draft_message_preview_text.dart'; export 'src/channel/stream_message_preview_text.dart'; diff --git a/sample_app/lib/routes/app_routes.dart b/sample_app/lib/routes/app_routes.dart index 3e11a35f2f..3cae1e3392 100644 --- a/sample_app/lib/routes/app_routes.dart +++ b/sample_app/lib/routes/app_routes.dart @@ -46,8 +46,9 @@ final appRoutes = [ builder: (context) { return (parentMessage != null) ? ThreadPage(parent: parentMessage) - : ChannelPage( + : StreamChannelPage( highlightInitialMessage: messageId != null, + isFloating: true, ); }, ), From 9e5ba935261887870102c13b6f96b21f24b59122 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 6 May 2026 17:36:26 +0200 Subject: [PATCH 02/48] simplify composer setup --- .../lib/src/channel/channel_page.dart | 148 ++++-------------- 1 file changed, 28 insertions(+), 120 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index 5b4ee980ab..602cfa9e6f 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -107,25 +107,34 @@ class _StreamChannelPageState extends State { return Scaffold( backgroundColor: colorTheme.appBg, appBar: appBar, - body: _FloatingChannelBody( - composer: composer, - typingIndicator: Container( - alignment: Alignment.centerLeft, - color: colorTheme.appBg.withOpacity(.9), - child: typingIndicator, - ), - messageListBuilder: (bottomWidgetsHeight) => StreamMessageListView( - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - highlightInitialMessage: widget.highlightInitialMessage, - onEditMessageTap: _editMessage, - onReplyTap: _reply, - swipeToReply: true, - threadBuilder: (_, parentMessage) { - return StreamThreadPage(parent: parentMessage!); - }, - bottomPadding: bottomWidgetsHeight, - ), + extendBody: true, + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.centerLeft, + color: colorTheme.appBg.withOpacity(.9), + child: typingIndicator, + ), + composer, + ], + ), + body: Builder( + builder: (context) { + final bottomInset = MediaQuery.of(context).padding.bottom; + return StreamMessageListView( + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + highlightInitialMessage: widget.highlightInitialMessage, + onEditMessageTap: _editMessage, + onReplyTap: _reply, + swipeToReply: true, + threadBuilder: (_, parentMessage) { + return StreamThreadPage(parent: parentMessage!); + }, + bottomPadding: bottomInset, + ); + }, ), ); } @@ -168,104 +177,3 @@ class _StreamChannelPageState extends State { ); } } - -// --------------------------------------------------------------------------- -// Floating layout helpers -// --------------------------------------------------------------------------- - -/// Layout slot identifiers for [_FloatingChannelBodyDelegate]. -enum _Slot { body, typing, composer } - -/// A custom [BoxConstraints] subclass that carries the composer height so that -/// a [LayoutBuilder] inside the body slot can read it without a separate -/// callback or [setState]. -/// -/// The [==] / [hashCode] overrides are intentional: they make the framework -/// re-run [RenderObject.performLayout] on the body child even when the outer -/// size constraints have not changed but only [composerHeight] has. -class _BodyBoxConstraints extends BoxConstraints { - const _BodyBoxConstraints({ - super.maxWidth, - super.maxHeight, - required this.composerHeight, - }) : assert(composerHeight >= 0, 'composerHeight must be non-negative'); - - /// Height of the floating composer, used as the list's bottom inset. - final double composerHeight; - - @override - bool operator ==(Object other) { - if (super != other) return false; - return other is _BodyBoxConstraints && other.composerHeight == composerHeight; - } - - @override - int get hashCode => Object.hash(super.hashCode, composerHeight); -} - -/// [MultiChildLayoutDelegate] that measures the composer first, then lays out -/// the typing indicator and body in the same layout pass — no post-frame -/// callback required. -class _FloatingChannelBodyDelegate extends MultiChildLayoutDelegate { - @override - void performLayout(Size size) { - // 1. Measure + position the composer at the bottom. - final composerSize = layoutChild(_Slot.composer, BoxConstraints.loose(size)); - positionChild(_Slot.composer, Offset(0, size.height - composerSize.height)); - - // 2. Measure + position the typing indicator just above the composer. - final typingSize = layoutChild(_Slot.typing, BoxConstraints(maxWidth: size.width)); - positionChild(_Slot.typing, Offset(0, size.height - composerSize.height - typingSize.height)); - - // 3. Lay out the body with the composer height embedded in the constraints - // so the LayoutBuilder inside can read it synchronously. - layoutChild( - _Slot.body, - _BodyBoxConstraints( - maxWidth: size.width, - maxHeight: size.height, - composerHeight: composerSize.height, - ), - ); - positionChild(_Slot.body, Offset.zero); - } - - @override - bool shouldRelayout(_FloatingChannelBodyDelegate oldDelegate) => false; -} - -/// Stateless widget that wires the floating layout together. -class _FloatingChannelBody extends StatelessWidget { - const _FloatingChannelBody({ - required this.composer, - required this.typingIndicator, - required this.messageListBuilder, - }); - - final Widget composer; - final Widget typingIndicator; - - /// Called with the current composer height so the list can set its inset. - final Widget Function(double bottomWidgetsHeight) messageListBuilder; - - @override - Widget build(BuildContext context) { - return CustomMultiChildLayout( - delegate: _FloatingChannelBodyDelegate(), - children: [ - // Body slot: a LayoutBuilder that reads the custom constraints. - LayoutId( - id: _Slot.body, - child: LayoutBuilder( - builder: (context, constraints) { - final height = constraints is _BodyBoxConstraints ? constraints.composerHeight : 0.0; - return messageListBuilder(height); - }, - ), - ), - LayoutId(id: _Slot.typing, child: typingIndicator), - LayoutId(id: _Slot.composer, child: composer), - ], - ); - } -} From e6e34444b3ca56edcd6e93607e8c109dad17aa0b Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 6 May 2026 17:40:57 +0200 Subject: [PATCH 03/48] poc for floating app bar --- .../lib/src/channel/channel_page.dart | 15 ++++++++++++--- .../src/message_list_view/message_list_view.dart | 13 ++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index 602cfa9e6f..08c7bb5963 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -104,10 +104,18 @@ class _StreamChannelPageState extends State { ); if (widget.isFloating) { + final floatingAppBar = StreamChannelHeader( + showTypingIndicator: false, + onBackPressed: widget.onBackPressed, + onImageTap: widget.onHeaderImageTap, + backgroundColor: Colors.transparent, + ); + return Scaffold( backgroundColor: colorTheme.appBg, - appBar: appBar, + appBar: floatingAppBar, extendBody: true, + extendBodyBehindAppBar: true, bottomNavigationBar: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -121,7 +129,7 @@ class _StreamChannelPageState extends State { ), body: Builder( builder: (context) { - final bottomInset = MediaQuery.of(context).padding.bottom; + final insets = MediaQuery.of(context).padding; return StreamMessageListView( initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, @@ -132,7 +140,8 @@ class _StreamChannelPageState extends State { threadBuilder: (_, parentMessage) { return StreamThreadPage(parent: parentMessage!); }, - bottomPadding: bottomInset, + topPadding: insets.top, + bottomPadding: insets.bottom, ); }, ), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index f97b7e5570..f9c9f722ac 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -125,6 +125,7 @@ class StreamMessageListView extends StatefulWidget { this.onMessageLongPress, this.config = const StreamMessageListViewConfiguration(), this.builders = const StreamMessageListViewBuilders(), + this.topPadding = 0, this.bottomPadding = 0, }); @@ -259,6 +260,12 @@ class StreamMessageListView extends StatefulWidget { /// Defaults to [StreamMessageListViewBuilders] with no overrides. final StreamMessageListViewBuilders builders; + /// Top padding added to the message list scroll view. + /// + /// Used by the floating app bar layout to keep the first message visible + /// below the app bar without requiring a separate [setState] call. + final double topPadding; + /// Bottom padding added to the message list scroll view. /// /// Used by the floating composer layout to keep the last message visible @@ -601,7 +608,7 @@ class _StreamMessageListViewState extends State { child: ScrollablePositionedList.separated( key: Key('mlv-${streamChannel?.channel.cid}-${widget.parentMessage?.id}'), padding: .only( - top: context.streamSpacing.sm, + top: max(widget.topPadding, context.streamSpacing.sm), bottom: max(widget.bottomPadding, context.streamSpacing.sm), ), keyboardDismissBehavior: widget.config.keyboardDismissBehavior, @@ -758,7 +765,7 @@ class _StreamMessageListViewState extends State { ), if (widget.config.showFloatingDateDivider) Positioned( - top: context.streamSpacing.sm, + top: max(widget.topPadding, context.streamSpacing.sm), child: FloatingDateDivider( itemCount: itemCount, reverse: widget.config.reverse, @@ -786,7 +793,7 @@ class _StreamMessageListViewState extends State { ), if (widget.config.showUnreadIndicator && !_isThreadConversation) Positioned( - top: context.streamSpacing.sm, + top: max(widget.topPadding, context.streamSpacing.sm), child: UnreadIndicatorButton( onJumpTap: scrollToUnreadDefaultTapAction, onDismissTap: _markMessagesAsRead, From c99bc631d5beedfca64974478d52ff79b96706f9 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 27 May 2026 11:05:44 +0200 Subject: [PATCH 04/48] fixes for merge issues --- .../lib/src/channel/channel_page.dart | 67 ++++++++----------- .../lib/src/channel/thread_page.dart | 19 +++--- 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index 08c7bb5963..01562c7011 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -13,8 +13,8 @@ class StreamChannelPage extends StatefulWidget { this.initialAlignment, this.highlightInitialMessage = false, this.onBackPressed, - this.onHeaderImageTap, - this.isFloating = false, + this.onChannelAvatarPressed, + this.isFloating = true, }); /// Initial scroll index for the message list. @@ -29,8 +29,8 @@ class StreamChannelPage extends StatefulWidget { /// Callback for when the back button is pressed. final VoidCallback? onBackPressed; - /// Callback for when the header image is tapped. - final VoidCallback? onHeaderImageTap; + /// Called when the default channel-avatar trailing is pressed. + final void Function(Channel channel)? onChannelAvatarPressed; /// Whether the composer floats over the message list. /// @@ -45,7 +45,7 @@ class StreamChannelPage extends StatefulWidget { class _StreamChannelPageState extends State { FocusNode? _focusNode; - final _messageInputController = StreamMessageInputController(); + final _messageComposerController = StreamMessageComposerController(); @override void initState() { @@ -60,14 +60,14 @@ class _StreamChannelPageState extends State { } void _reply(Message message) { - _messageInputController.quotedMessage = message; + _messageComposerController.quotedMessage = message; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _focusNode!.requestFocus(); }); } void _editMessage(Message message) { - _messageInputController.editMessage(message); + _messageComposerController.editMessage(message); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _focusNode!.requestFocus(); }); @@ -75,45 +75,30 @@ class _StreamChannelPageState extends State { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - final textTheme = theme.textTheme; - final colorTheme = theme.colorTheme; - - final appBar = StreamChannelHeader( - showTypingIndicator: false, - onBackPressed: widget.onBackPressed, - onImageTap: widget.onHeaderImageTap, - ); - final typingIndicator = StreamTypingIndicator( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), - style: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, + style: context.streamTextTheme.captionDefault.copyWith( + color: context.streamColorScheme.textSecondary, ), ); final composer = StreamMessageComposer( focusNode: _focusNode, - messageInputController: _messageInputController, - onQuotedMessageCleared: _messageInputController.clearQuotedMessage, + messageComposerController: _messageComposerController, + onQuotedMessageCleared: _messageComposerController.clearQuotedMessage, enableVoiceRecording: true, isFloating: widget.isFloating, ); if (widget.isFloating) { - final floatingAppBar = StreamChannelHeader( - showTypingIndicator: false, - onBackPressed: widget.onBackPressed, - onImageTap: widget.onHeaderImageTap, - backgroundColor: Colors.transparent, - ); - return Scaffold( - backgroundColor: colorTheme.appBg, - appBar: floatingAppBar, + backgroundColor: context.streamColorScheme.backgroundApp, + appBar: StreamChannelHeader( + onChannelAvatarPressed: widget.onChannelAvatarPressed, + ), extendBody: true, extendBodyBehindAppBar: true, bottomNavigationBar: Column( @@ -121,7 +106,7 @@ class _StreamChannelPageState extends State { children: [ Container( alignment: Alignment.centerLeft, - color: colorTheme.appBg.withOpacity(.9), + color: context.streamColorScheme.backgroundApp.withOpacity(.9), child: typingIndicator, ), composer, @@ -133,10 +118,12 @@ class _StreamChannelPageState extends State { return StreamMessageListView( initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, - highlightInitialMessage: widget.highlightInitialMessage, + config: StreamMessageListViewConfiguration( + highlightInitialMessage: widget.highlightInitialMessage, + swipeToReply: true, + ), onEditMessageTap: _editMessage, onReplyTap: _reply, - swipeToReply: true, threadBuilder: (_, parentMessage) { return StreamThreadPage(parent: parentMessage!); }, @@ -149,8 +136,10 @@ class _StreamChannelPageState extends State { } return Scaffold( - backgroundColor: colorTheme.appBg, - appBar: appBar, + backgroundColor: context.streamColorScheme.backgroundApp, + appBar: StreamChannelHeader( + onChannelAvatarPressed: widget.onChannelAvatarPressed, + ), body: Column( children: [ Expanded( @@ -159,10 +148,12 @@ class _StreamChannelPageState extends State { StreamMessageListView( initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, - highlightInitialMessage: widget.highlightInitialMessage, + config: StreamMessageListViewConfiguration( + highlightInitialMessage: widget.highlightInitialMessage, + swipeToReply: true, + ), onEditMessageTap: _editMessage, onReplyTap: _reply, - swipeToReply: true, threadBuilder: (_, parentMessage) { return StreamThreadPage(parent: parentMessage!); }, @@ -173,7 +164,7 @@ class _StreamChannelPageState extends State { right: 0, child: Container( alignment: Alignment.centerLeft, - color: colorTheme.appBg.withOpacity(.9), + color: context.streamColorScheme.backgroundApp.withOpacity(.9), child: typingIndicator, ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart index c6797a901f..ead1d8f08c 100644 --- a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart @@ -30,12 +30,12 @@ class StreamThreadPage extends StatefulWidget { class _StreamThreadPageState extends State { final FocusNode _focusNode = FocusNode(); - late StreamMessageInputController _messageInputController; + late StreamMessageComposerController _messageComposerController; @override void initState() { super.initState(); - _messageInputController = StreamMessageInputController( + _messageComposerController = StreamMessageComposerController( message: Message(parentId: widget.parent.id), ); } @@ -43,11 +43,12 @@ class _StreamThreadPageState extends State { @override void dispose() { _focusNode.dispose(); + _messageComposerController.dispose(); super.dispose(); } void _reply(Message message) { - _messageInputController.quotedMessage = message; + _messageComposerController.quotedMessage = message; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _focusNode.requestFocus(); }); @@ -56,7 +57,7 @@ class _StreamThreadPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, + backgroundColor: context.streamColorScheme.backgroundApp, appBar: StreamThreadHeader(parent: widget.parent), body: Column( children: [ @@ -66,16 +67,18 @@ class _StreamThreadPageState extends State { initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, onReplyTap: _reply, - swipeToReply: true, - showScrollToBottom: false, - highlightInitialMessage: true, + config: const StreamMessageListViewConfiguration( + swipeToReply: true, + showScrollToBottom: false, + highlightInitialMessage: true, + ), onViewInChannelTap: widget.onViewInChannelTap, ), ), if (widget.parent.type != 'deleted') StreamMessageComposer( focusNode: _focusNode, - messageInputController: _messageInputController, + messageComposerController: _messageComposerController, enableVoiceRecording: true, ), ], From 905b8684c3d775e413a4259348b688e238ed6289 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 27 May 2026 11:07:59 +0200 Subject: [PATCH 05/48] Revert "simplify composer setup" This reverts commit 8f3f336471607690ecfc9305af9be1b5ed99aaa3. # Conflicts: # packages/stream_chat_flutter/lib/src/channel/channel_page.dart --- .../lib/src/channel/channel_page.dart | 152 ++++++++++++++---- 1 file changed, 121 insertions(+), 31 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index 01562c7011..19b8ea5648 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -99,38 +99,27 @@ class _StreamChannelPageState extends State { appBar: StreamChannelHeader( onChannelAvatarPressed: widget.onChannelAvatarPressed, ), - extendBody: true, - extendBodyBehindAppBar: true, - bottomNavigationBar: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - alignment: Alignment.centerLeft, - color: context.streamColorScheme.backgroundApp.withOpacity(.9), - child: typingIndicator, + body: _FloatingChannelBody( + composer: composer, + typingIndicator: Container( + alignment: Alignment.centerLeft, + color: context.streamColorScheme.backgroundApp.withOpacity(.9), + child: typingIndicator, + ), + messageListBuilder: (bottomWidgetsHeight) => StreamMessageListView( + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + config: StreamMessageListViewConfiguration( + highlightInitialMessage: widget.highlightInitialMessage, + swipeToReply: true, ), - composer, - ], - ), - body: Builder( - builder: (context) { - final insets = MediaQuery.of(context).padding; - return StreamMessageListView( - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - config: StreamMessageListViewConfiguration( - highlightInitialMessage: widget.highlightInitialMessage, - swipeToReply: true, - ), - onEditMessageTap: _editMessage, - onReplyTap: _reply, - threadBuilder: (_, parentMessage) { - return StreamThreadPage(parent: parentMessage!); - }, - topPadding: insets.top, - bottomPadding: insets.bottom, - ); - }, + onEditMessageTap: _editMessage, + onReplyTap: _reply, + threadBuilder: (_, parentMessage) { + return StreamThreadPage(parent: parentMessage!); + }, + bottomPadding: bottomWidgetsHeight, + ), ), ); } @@ -177,3 +166,104 @@ class _StreamChannelPageState extends State { ); } } + +// --------------------------------------------------------------------------- +// Floating layout helpers +// --------------------------------------------------------------------------- + +/// Layout slot identifiers for [_FloatingChannelBodyDelegate]. +enum _Slot { body, typing, composer } + +/// A custom [BoxConstraints] subclass that carries the composer height so that +/// a [LayoutBuilder] inside the body slot can read it without a separate +/// callback or [setState]. +/// +/// The [==] / [hashCode] overrides are intentional: they make the framework +/// re-run [RenderObject.performLayout] on the body child even when the outer +/// size constraints have not changed but only [composerHeight] has. +class _BodyBoxConstraints extends BoxConstraints { + const _BodyBoxConstraints({ + super.maxWidth, + super.maxHeight, + required this.composerHeight, + }) : assert(composerHeight >= 0, 'composerHeight must be non-negative'); + + /// Height of the floating composer, used as the list's bottom inset. + final double composerHeight; + + @override + bool operator ==(Object other) { + if (super != other) return false; + return other is _BodyBoxConstraints && other.composerHeight == composerHeight; + } + + @override + int get hashCode => Object.hash(super.hashCode, composerHeight); +} + +/// [MultiChildLayoutDelegate] that measures the composer first, then lays out +/// the typing indicator and body in the same layout pass — no post-frame +/// callback required. +class _FloatingChannelBodyDelegate extends MultiChildLayoutDelegate { + @override + void performLayout(Size size) { + // 1. Measure + position the composer at the bottom. + final composerSize = layoutChild(_Slot.composer, BoxConstraints.loose(size)); + positionChild(_Slot.composer, Offset(0, size.height - composerSize.height)); + + // 2. Measure + position the typing indicator just above the composer. + final typingSize = layoutChild(_Slot.typing, BoxConstraints(maxWidth: size.width)); + positionChild(_Slot.typing, Offset(0, size.height - composerSize.height - typingSize.height)); + + // 3. Lay out the body with the composer height embedded in the constraints + // so the LayoutBuilder inside can read it synchronously. + layoutChild( + _Slot.body, + _BodyBoxConstraints( + maxWidth: size.width, + maxHeight: size.height, + composerHeight: composerSize.height, + ), + ); + positionChild(_Slot.body, Offset.zero); + } + + @override + bool shouldRelayout(_FloatingChannelBodyDelegate oldDelegate) => false; +} + +/// Stateless widget that wires the floating layout together. +class _FloatingChannelBody extends StatelessWidget { + const _FloatingChannelBody({ + required this.composer, + required this.typingIndicator, + required this.messageListBuilder, + }); + + final Widget composer; + final Widget typingIndicator; + + /// Called with the current composer height so the list can set its inset. + final Widget Function(double bottomWidgetsHeight) messageListBuilder; + + @override + Widget build(BuildContext context) { + return CustomMultiChildLayout( + delegate: _FloatingChannelBodyDelegate(), + children: [ + // Body slot: a LayoutBuilder that reads the custom constraints. + LayoutId( + id: _Slot.body, + child: LayoutBuilder( + builder: (context, constraints) { + final height = constraints is _BodyBoxConstraints ? constraints.composerHeight : 0.0; + return messageListBuilder(height); + }, + ), + ), + LayoutId(id: _Slot.typing, child: typingIndicator), + LayoutId(id: _Slot.composer, child: composer), + ], + ); + } +} From bb28a92c8736f85c70dc76df22b7b600244b2ab3 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 27 May 2026 11:47:38 +0200 Subject: [PATCH 06/48] fix avatar press --- .../lib/src/channel/channel_page.dart | 7 +++--- sample_app/lib/routes/app_routes.dart | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index 19b8ea5648..3befad17c3 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -30,7 +30,7 @@ class StreamChannelPage extends StatefulWidget { final VoidCallback? onBackPressed; /// Called when the default channel-avatar trailing is pressed. - final void Function(Channel channel)? onChannelAvatarPressed; + final void Function(BuildContext context, Channel channel)? onChannelAvatarPressed; /// Whether the composer floats over the message list. /// @@ -97,8 +97,9 @@ class _StreamChannelPageState extends State { return Scaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: StreamChannelHeader( - onChannelAvatarPressed: widget.onChannelAvatarPressed, + onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), ), + extendBodyBehindAppBar: false, body: _FloatingChannelBody( composer: composer, typingIndicator: Container( @@ -127,7 +128,7 @@ class _StreamChannelPageState extends State { return Scaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: StreamChannelHeader( - onChannelAvatarPressed: widget.onChannelAvatarPressed, + onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), ), body: Column( children: [ diff --git a/sample_app/lib/routes/app_routes.dart b/sample_app/lib/routes/app_routes.dart index 3cae1e3392..2e9e2029ff 100644 --- a/sample_app/lib/routes/app_routes.dart +++ b/sample_app/lib/routes/app_routes.dart @@ -49,6 +49,31 @@ final appRoutes = [ : StreamChannelPage( highlightInitialMessage: messageId != null, isFloating: true, + onChannelAvatarPressed: (context, channel) { + final isOneToOne = channel.isOneToOne; + final currentUserId = StreamChat.of(context).currentUser?.id; + + final channelMembers = channel.state?.members ?? []; + final otherUser = isOneToOne + ? channelMembers.firstWhere((m) => m.userId != currentUserId).user + : null; + + final router = GoRouter.of(context); + + if (otherUser != null) { + router.pushNamed( + Routes.CHAT_INFO_SCREEN.name, + pathParameters: Routes.CHAT_INFO_SCREEN.params(channel), + extra: otherUser, + ); + return; + } + + router.pushNamed( + Routes.GROUP_INFO_SCREEN.name, + pathParameters: Routes.GROUP_INFO_SCREEN.params(channel), + ); + }, ); }, ), From c6b4840e34bd012fa7d5fc0c8c036cf86263fd01 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 27 May 2026 13:53:40 +0200 Subject: [PATCH 07/48] fix composer safeArea --- .../lib/src/message_input/stream_message_composer.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index c59327dcb9..9c18fad145 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -806,7 +806,6 @@ class DefaultStreamMessageComposerState extends State Date: Wed, 27 May 2026 14:04:03 +0200 Subject: [PATCH 08/48] Add floating header --- .../lib/src/channel/channel_header.dart | 7 +- .../lib/src/channel/channel_page.dart | 88 +++++++------------ .../lib/src/misc/back_button.dart | 7 +- 3 files changed, 46 insertions(+), 56 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index 3d1fb1e3e5..5e0c6bfd0a 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -81,6 +81,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget this.trailing, this.primary = true, this.style, + this.floating = false, }); /// Called when the default channel-avatar trailing is pressed. @@ -129,6 +130,9 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget /// [StreamChatThemeData.channelHeaderTheme]. final StreamAppBarStyle? style; + /// Whether the header is floating. + final bool floating; + @override Size get preferredSize => const Size.fromHeight(kStreamToolbarHeight); @@ -139,7 +143,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget var leading = this.leading; if (leading == null && automaticallyImplyLeading) { - leading = const StreamBackButton(showUnreadCount: true); + leading = StreamBackButton(showUnreadCount: true, floating: floating); } var title = this.title; @@ -187,6 +191,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget trailing: trailing, primary: primary, style: style, + floating: floating, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index 3befad17c3..f9dc58a029 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -93,70 +93,43 @@ class _StreamChannelPageState extends State { isFloating: widget.isFloating, ); - if (widget.isFloating) { - return Scaffold( - backgroundColor: context.streamColorScheme.backgroundApp, - appBar: StreamChannelHeader( - onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), - ), - extendBodyBehindAppBar: false, - body: _FloatingChannelBody( - composer: composer, - typingIndicator: Container( - alignment: Alignment.centerLeft, - color: context.streamColorScheme.backgroundApp.withOpacity(.9), - child: typingIndicator, - ), - messageListBuilder: (bottomWidgetsHeight) => StreamMessageListView( - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - config: StreamMessageListViewConfiguration( - highlightInitialMessage: widget.highlightInitialMessage, - swipeToReply: true, - ), - onEditMessageTap: _editMessage, - onReplyTap: _reply, - threadBuilder: (_, parentMessage) { - return StreamThreadPage(parent: parentMessage!); - }, - bottomPadding: bottomWidgetsHeight, - ), - ), - ); - } + final appBar = StreamChannelHeader( + onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), + floating: widget.isFloating, + ); - return Scaffold( - backgroundColor: context.streamColorScheme.backgroundApp, - appBar: StreamChannelHeader( - onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), + StreamMessageListView messageListBuilder(double bottomPadding) => StreamMessageListView( + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + config: StreamMessageListViewConfiguration( + highlightInitialMessage: widget.highlightInitialMessage, + swipeToReply: true, + ), + onEditMessageTap: _editMessage, + onReplyTap: _reply, + threadBuilder: (_, parentMessage) { + return StreamThreadPage(parent: parentMessage!); + }, + bottomPadding: bottomPadding, + ); + + final body = switch (widget.isFloating) { + true => _FloatingChannelBody( + composer: composer, + typingIndicator: typingIndicator, + messageListBuilder: messageListBuilder, ), - body: Column( + false => Column( children: [ Expanded( child: Stack( children: [ - StreamMessageListView( - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - config: StreamMessageListViewConfiguration( - highlightInitialMessage: widget.highlightInitialMessage, - swipeToReply: true, - ), - onEditMessageTap: _editMessage, - onReplyTap: _reply, - threadBuilder: (_, parentMessage) { - return StreamThreadPage(parent: parentMessage!); - }, - ), + messageListBuilder(0), Positioned( bottom: 0, left: 0, right: 0, - child: Container( - alignment: Alignment.centerLeft, - color: context.streamColorScheme.backgroundApp.withOpacity(.9), - child: typingIndicator, - ), + child: typingIndicator, ), ], ), @@ -164,6 +137,13 @@ class _StreamChannelPageState extends State { composer, ], ), + }; + + return Scaffold( + backgroundColor: context.streamColorScheme.backgroundApp, + appBar: appBar, + extendBodyBehindAppBar: widget.isFloating, + body: body, ); } } diff --git a/packages/stream_chat_flutter/lib/src/misc/back_button.dart b/packages/stream_chat_flutter/lib/src/misc/back_button.dart index 7b440227e2..2910fc903e 100644 --- a/packages/stream_chat_flutter/lib/src/misc/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/misc/back_button.dart @@ -11,6 +11,7 @@ class StreamBackButton extends StatelessWidget { this.onPressed, this.showUnreadCount = false, this.channelId, + this.floating = false, }); /// Callback for when button is pressed @@ -22,6 +23,9 @@ class StreamBackButton extends StatelessWidget { /// Channel ID used to retrieve unread count final String? channelId; + /// Whether the back button is floating. + final bool floating; + @override Widget build(BuildContext context) { final iconData = switch (Theme.of(context).platform) { @@ -30,7 +34,8 @@ class StreamBackButton extends StatelessWidget { }; Widget button = StreamButton.icon( - type: .ghost, + type: floating ? .outline : .ghost, + isFloating: floating, size: .medium, style: .secondary, icon: Icon(iconData), From 66af71247da87628da905dc2ffa2be32ff93634d Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 27 May 2026 14:57:23 +0200 Subject: [PATCH 09/48] Cleanup sample app --- .../lib/src/channel/channel_page.dart | 2 +- .../lib/stream_chat_flutter.dart | 1 + sample_app/lib/pages/channel_page.dart | 209 ------------------ sample_app/lib/pages/draft_list_page.dart | 6 +- sample_app/lib/pages/thread_list_page.dart | 3 +- sample_app/lib/pages/thread_page.dart | 86 ------- sample_app/lib/routes/app_routes.dart | 6 +- 7 files changed, 7 insertions(+), 306 deletions(-) delete mode 100644 sample_app/lib/pages/channel_page.dart delete mode 100644 sample_app/lib/pages/thread_page.dart diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index f9dc58a029..7a1fcaf18b 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -14,7 +14,7 @@ class StreamChannelPage extends StatefulWidget { this.highlightInitialMessage = false, this.onBackPressed, this.onChannelAvatarPressed, - this.isFloating = true, + this.isFloating = false, }); /// Initial scroll index for the message list. diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index de2b6ac64d..a3184b341f 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -172,6 +172,7 @@ export 'src/channel/channel_page.dart'; export 'src/channel/stream_channel_name.dart'; export 'src/channel/stream_draft_message_preview_text.dart'; export 'src/channel/stream_message_preview_text.dart'; +export 'src/channel/thread_page.dart'; // region SDK Design Refresh Components export 'src/components/avatar/stream_channel_avatar.dart'; export 'src/components/avatar/stream_user_avatar.dart'; diff --git a/sample_app/lib/pages/channel_page.dart b/sample_app/lib/pages/channel_page.dart deleted file mode 100644 index b04d751638..0000000000 --- a/sample_app/lib/pages/channel_page.dart +++ /dev/null @@ -1,209 +0,0 @@ -// ignore_for_file: deprecated_member_use, avoid_redundant_argument_values - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sample_app/config/sample_app_config.dart'; -import 'package:sample_app/pages/thread_page.dart'; -import 'package:sample_app/routes/routes.dart'; -import 'package:sample_app/widgets/location/location_picker_dialog.dart'; -import 'package:sample_app/widgets/location/location_picker_option.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -class ChannelPage extends StatefulWidget { - const ChannelPage({ - super.key, - this.initialScrollIndex, - this.initialAlignment, - this.highlightInitialMessage = false, - }); - final int? initialScrollIndex; - final double? initialAlignment; - final bool highlightInitialMessage; - - @override - State createState() => _ChannelPageState(); -} - -class _ChannelPageState extends State { - FocusNode? _focusNode; - final _messageComposerController = StreamMessageComposerController(); - - @override - void initState() { - _focusNode = FocusNode(); - super.initState(); - } - - @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 channel = StreamChannel.of(context).channel; - final config = channel.config; - - return Scaffold( - backgroundColor: context.streamColorScheme.backgroundApp, - appBar: StreamChannelHeader( - onChannelAvatarPressed: (channel) { - final isOneToOne = channel.isOneToOne; - final currentUserId = StreamChat.of(context).currentUser?.id; - - final channelMembers = channel.state?.members ?? []; - final otherUser = isOneToOne ? channelMembers.firstWhere((m) => m.userId != currentUserId).user : null; - - _pushChannelInfo(context, channel, otherUser); - }, - ), - body: Column( - children: [ - Expanded( - child: Stack( - children: [ - StreamMessageListView( - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - config: StreamMessageListViewConfiguration( - swipeToReply: true, - highlightInitialMessage: widget.highlightInitialMessage, - ), - onEditMessageTap: _editMessage, - onReplyTap: _reply, - threadBuilder: (_, parentMessage) { - return ThreadPage(parent: parentMessage!); - }, - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - alignment: Alignment.centerLeft, - color: context.streamColorScheme.backgroundApp.withOpacity(.9), - child: StreamTypingIndicator( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - style: context.streamTextTheme.captionDefault.copyWith( - color: context.streamColorScheme.textSecondary, - ), - ), - ), - ), - ], - ), - ), - Builder( - builder: (context) { - final appConfig = context.sampleAppConfig; - final locationEnabled = - appConfig.enableLocationSharing && config?.sharedLocations == true && channel.canShareLocation; - - return StreamMessageComposer( - focusNode: _focusNode, - messageComposerController: _messageComposerController, - onQuotedMessageCleared: _messageComposerController.clearQuotedMessage, - enableVoiceRecording: true, - allowedAttachmentPickerTypes: [ - ...AttachmentPickerType.values, - if (locationEnabled) const LocationPickerType(), - ], - onAttachmentPickerResult: (result) { - return _onCustomAttachmentPickerResult(channel, result); - }, - attachmentPickerOptionsBuilder: (context, defaultOptions) => [ - ...defaultOptions, - if (locationEnabled) - TabbedAttachmentPickerOption( - key: 'location-picker', - icon: context.streamIcons.location, - supportedTypes: [const LocationPickerType()], - isEnabled: (value) { - if (value.isEmpty) return true; - return value.extraData['location'] != null; - }, - optionViewBuilder: (context, controller) => LocationPicker( - onLocationPicked: (locationResult) { - if (locationResult == null) return; - - controller.notifyCustomResult( - LocationPicked(location: locationResult), - ); - }, - ), - ), - ], - ); - }, - ), - ], - ), - ); - } - - bool _onCustomAttachmentPickerResult( - Channel channel, - StreamAttachmentPickerResult result, - ) { - if (result is LocationPicked) { - _onShareLocationPicked(channel, result.location).ignore(); - return true; // Notify that the result was handled. - } - - return false; // Notify that the result was not handled. - } - - Future _onShareLocationPicked( - Channel channel, - LocationPickerResult result, - ) async { - if (result.endSharingAt case final endSharingAt?) { - return channel.startLiveLocationSharing( - endSharingAt: endSharingAt, - location: result.coordinates, - ); - } - - return channel.sendStaticLocation(location: result.coordinates); - } -} - -// Pushes the chat / group info screen depending on whether [user] was -// resolved. 1-1 channels pass the other member here (forwarded as `extra` -// to the chat-info route); group channels pass `null` and route to the -// group info screen. -Future _pushChannelInfo(BuildContext context, Channel channel, User? user) { - final router = GoRouter.of(context); - - if (user != null) { - return router.pushNamed( - Routes.CHAT_INFO_SCREEN.name, - pathParameters: Routes.CHAT_INFO_SCREEN.params(channel), - extra: user, - ); - } - - return router.pushNamed( - Routes.GROUP_INFO_SCREEN.name, - pathParameters: Routes.GROUP_INFO_SCREEN.params(channel), - ); -} diff --git a/sample_app/lib/pages/draft_list_page.dart b/sample_app/lib/pages/draft_list_page.dart index e486ac9d05..fa50bf2464 100644 --- a/sample_app/lib/pages/draft_list_page.dart +++ b/sample_app/lib/pages/draft_list_page.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:sample_app/pages/channel_page.dart'; -import 'package:sample_app/pages/thread_page.dart'; import 'package:sample_app/widgets/stream_draft_list_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -71,10 +69,10 @@ class _DraftListPageState extends State { channel: channel, initialMessageId: draft.parentId, child: switch (draft.parentMessage) { - final parent? => ThreadPage( + final parent? => StreamThreadPage( parent: parent.copyWith(draft: draft), ), - _ => const ChannelPage(), + _ => const StreamChannelPage(), }, ); }, diff --git a/sample_app/lib/pages/thread_list_page.dart b/sample_app/lib/pages/thread_list_page.dart index ef0bd215a3..b9cfaf29f8 100644 --- a/sample_app/lib/pages/thread_list_page.dart +++ b/sample_app/lib/pages/thread_list_page.dart @@ -1,7 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:sample_app/pages/thread_page.dart'; import 'package:sample_app/routes/routes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -63,7 +62,7 @@ class _ThreadListPageState extends State { .where((msg) => msg != null) .cast(), builder: (_, parentMessage) { - return ThreadPage( + return StreamThreadPage( parent: parentMessage, onViewInChannelTap: (message) { GoRouter.of(context).goNamed( diff --git a/sample_app/lib/pages/thread_page.dart b/sample_app/lib/pages/thread_page.dart deleted file mode 100644 index f2bd9711e9..0000000000 --- a/sample_app/lib/pages/thread_page.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -class ThreadPage extends StatefulWidget { - const ThreadPage({ - super.key, - required this.parent, - this.initialScrollIndex, - this.initialAlignment, - this.onViewInChannelTap, - }); - final Message parent; - final int? initialScrollIndex; - final double? initialAlignment; - final void Function(Message message)? onViewInChannelTap; - - @override - State createState() => _ThreadPageState(); -} - -class _ThreadPageState extends State { - final FocusNode _focusNode = FocusNode(); - late StreamMessageComposerController _messageComposerController; - - @override - void initState() { - super.initState(); - _messageComposerController = StreamMessageComposerController( - message: Message(parentId: widget.parent.id), - ); - } - - @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) { - return Scaffold( - backgroundColor: context.streamColorScheme.backgroundApp, - appBar: StreamThreadHeader(parent: widget.parent), - body: Column( - children: [ - Expanded( - child: StreamMessageListView( - parentMessage: widget.parent, - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - onReplyTap: _reply, - onEditMessageTap: _editMessage, - config: const StreamMessageListViewConfiguration( - swipeToReply: true, - showScrollToBottom: false, - highlightInitialMessage: true, - ), - onViewInChannelTap: widget.onViewInChannelTap, - ), - ), - if (widget.parent.type != 'deleted') - StreamMessageComposer( - focusNode: _focusNode, - messageComposerController: _messageComposerController, - enableVoiceRecording: true, - ), - ], - ), - ); - } -} diff --git a/sample_app/lib/routes/app_routes.dart b/sample_app/lib/routes/app_routes.dart index 2e9e2029ff..13ad07a72a 100644 --- a/sample_app/lib/routes/app_routes.dart +++ b/sample_app/lib/routes/app_routes.dart @@ -3,14 +3,12 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sample_app/pages/advanced_options_page.dart'; import 'package:sample_app/pages/channel_list_page.dart'; -import 'package:sample_app/pages/channel_page.dart'; import 'package:sample_app/pages/chat_info_screen.dart'; import 'package:sample_app/pages/choose_user_page.dart'; import 'package:sample_app/pages/group_chat_details_screen.dart'; import 'package:sample_app/pages/group_info_screen.dart'; import 'package:sample_app/pages/new_chat_screen.dart'; import 'package:sample_app/pages/new_group_chat_screen.dart'; -import 'package:sample_app/pages/thread_page.dart'; import 'package:sample_app/routes/routes.dart'; import 'package:sample_app/state/new_group_chat_state.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -45,10 +43,10 @@ final appRoutes = [ child: Builder( builder: (context) { return (parentMessage != null) - ? ThreadPage(parent: parentMessage) + ? StreamThreadPage(parent: parentMessage) : StreamChannelPage( highlightInitialMessage: messageId != null, - isFloating: true, + isFloating: false, onChannelAvatarPressed: (context, channel) { final isOneToOne = channel.isOneToOne; final currentUserId = StreamChat.of(context).currentUser?.id; From 2c156b21e66b85ce5aa51a2d65f7d99a686f0da5 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 27 May 2026 15:55:53 +0200 Subject: [PATCH 10/48] Fix topPadding on listview --- .../stream_chat_flutter/lib/src/channel/channel_page.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index 7a1fcaf18b..bec5116152 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -1,7 +1,6 @@ // ignore_for_file: deprecated_member_use, avoid_redundant_argument_values import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/channel/thread_page.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A channel page with optional floating composer support. @@ -98,6 +97,8 @@ class _StreamChannelPageState extends State { floating: widget.isFloating, ); + final topPadding = widget.isFloating ? appBar.preferredSize.height + MediaQuery.of(context).padding.top : 0.0; + StreamMessageListView messageListBuilder(double bottomPadding) => StreamMessageListView( initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, @@ -110,6 +111,7 @@ class _StreamChannelPageState extends State { threadBuilder: (_, parentMessage) { return StreamThreadPage(parent: parentMessage!); }, + topPadding: topPadding, bottomPadding: bottomPadding, ); From d42cc51365c41af78c48adc4267450135139ba52 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 27 May 2026 16:07:09 +0200 Subject: [PATCH 11/48] Fix attachment picker padding --- .../lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart index 1b224a1726..f18f1e479f 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart @@ -321,7 +321,7 @@ class StreamPhotoGallery extends StatelessWidget { primary: primary, physics: physics, shrinkWrap: shrinkWrap, - padding: padding, + padding: padding ?? EdgeInsets.only(top: 0, bottom: MediaQuery.paddingOf(context).bottom), scrollController: scrollController, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, From 68f1ed060315b4c9159c0302636c0463e993ccea Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 28 May 2026 15:21:10 +0200 Subject: [PATCH 12/48] Using theming for floating config --- .../lib/src/channel/channel_header.dart | 10 ++++---- .../lib/src/channel/channel_page.dart | 18 ++++----------- .../stream_message_composer.dart | 23 +++++++++++++------ sample_app/lib/routes/app_routes.dart | 1 - 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index 5e0c6bfd0a..7968d91747 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -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. @@ -81,7 +82,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget this.trailing, this.primary = true, this.style, - this.floating = false, + this.appBarBehavior, }); /// Called when the default channel-avatar trailing is pressed. @@ -131,7 +132,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget final StreamAppBarStyle? style; /// Whether the header is floating. - final bool floating; + final AppBarBehavior? appBarBehavior; @override Size get preferredSize => const Size.fromHeight(kStreamToolbarHeight); @@ -143,7 +144,8 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget var leading = this.leading; if (leading == null && automaticallyImplyLeading) { - leading = StreamBackButton(showUnreadCount: true, floating: floating); + final effectiveAppBarBehavior = appBarBehavior ?? StreamTheme.of(context).appStyle.appBarBehavior; + leading = StreamBackButton(showUnreadCount: true, floating: effectiveAppBarBehavior == .floating); } var title = this.title; @@ -191,7 +193,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget trailing: trailing, primary: primary, style: style, - floating: floating, + appBarBehavior: appBarBehavior, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index bec5116152..c6f438e1d5 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -13,7 +13,6 @@ class StreamChannelPage extends StatefulWidget { this.highlightInitialMessage = false, this.onBackPressed, this.onChannelAvatarPressed, - this.isFloating = false, }); /// Initial scroll index for the message list. @@ -31,13 +30,6 @@ class StreamChannelPage extends StatefulWidget { /// Called when the default channel-avatar trailing is pressed. final void Function(BuildContext context, Channel channel)? onChannelAvatarPressed; - /// Whether the composer floats over the message list. - /// - /// When true the composer is overlaid at the bottom and the message list - /// scrolls underneath it. Layout is done in a single pass so the list - /// inset and the composer height are always in sync. - final bool isFloating; - @override State createState() => _StreamChannelPageState(); } @@ -89,15 +81,15 @@ class _StreamChannelPageState extends State { messageComposerController: _messageComposerController, onQuotedMessageCleared: _messageComposerController.clearQuotedMessage, enableVoiceRecording: true, - isFloating: widget.isFloating, ); + final isFloating = StreamTheme.of(context).appStyle.appBarBehavior == .floating; + final appBar = StreamChannelHeader( onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), - floating: widget.isFloating, ); - final topPadding = widget.isFloating ? appBar.preferredSize.height + MediaQuery.of(context).padding.top : 0.0; + final topPadding = isFloating ? appBar.preferredSize.height + MediaQuery.of(context).padding.top : 0.0; StreamMessageListView messageListBuilder(double bottomPadding) => StreamMessageListView( initialScrollIndex: widget.initialScrollIndex, @@ -115,7 +107,7 @@ class _StreamChannelPageState extends State { bottomPadding: bottomPadding, ); - final body = switch (widget.isFloating) { + final body = switch (isFloating) { true => _FloatingChannelBody( composer: composer, typingIndicator: typingIndicator, @@ -144,7 +136,7 @@ class _StreamChannelPageState extends State { return Scaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: appBar, - extendBodyBehindAppBar: widget.isFloating, + extendBodyBehindAppBar: isFloating, body: body, ); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 9c18fad145..c8428f1e9a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/message_input/error_alert_sheet.dart'; import 'package:stream_chat_flutter/src/message_input/stream_chat_message_input.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; const _kCommandTrigger = '/'; const _kMentionTrigger = '@'; @@ -95,7 +96,7 @@ class StreamMessageComposer extends StatelessWidget { TextCapitalization textCapitalization = TextCapitalization.sentences, bool autofocus = false, bool autoCorrect = true, - bool isFloating = false, + ComposerLocation? composerLocation, }) : props = .new( onMessageSent: onMessageSent, preMessageSending: preMessageSending, @@ -131,7 +132,7 @@ class StreamMessageComposer extends StatelessWidget { textCapitalization: textCapitalization, autofocus: autofocus, autoCorrect: autoCorrect, - isFloating: isFloating, + composerLocation: composerLocation, ); /// Creates a [StreamMessageComposer] from a pre-built [MessageComposerProps]. @@ -192,7 +193,7 @@ class MessageComposerProps { this.textCapitalization = TextCapitalization.sentences, this.autofocus = false, this.autoCorrect = true, - this.isFloating = false, + this.composerLocation, }); /// Function called after sending the message. @@ -387,8 +388,8 @@ class MessageComposerProps { /// Defaults to true. final bool autoCorrect; - /// Whether the message composer is floating. - final bool isFloating; + /// The location of the message composer. + final ComposerLocation? composerLocation; /// Returns a copy of this [MessageComposerProps] with the given fields /// replaced with new values. MessageComposerProps copyWith({ @@ -793,11 +794,17 @@ class DefaultStreamMessageComposerState extends State null, + .docked => context.streamColorScheme.backgroundElevation1, + }, ), child: AnimatedBuilder( animation: _pickerAnimation, @@ -884,6 +891,8 @@ class DefaultStreamMessageComposerState extends State Date: Thu, 28 May 2026 16:46:59 +0200 Subject: [PATCH 13/48] improve using default floatings from theme --- .../lib/src/channel/channel_header.dart | 3 +-- .../lib/src/channel/thread_page.dart | 7 ++++++- .../lib/src/misc/back_button.dart | 12 ++++++++---- .../stream_chat_flutter/lib/stream_chat_flutter.dart | 3 +++ sample_app/lib/app.dart | 12 ++++++++++-- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index 7968d91747..ccc4cfb49d 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -144,8 +144,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget var leading = this.leading; if (leading == null && automaticallyImplyLeading) { - final effectiveAppBarBehavior = appBarBehavior ?? StreamTheme.of(context).appStyle.appBarBehavior; - leading = StreamBackButton(showUnreadCount: true, floating: effectiveAppBarBehavior == .floating); + leading = StreamBackButton(showUnreadCount: true, appBarBehavior: appBarBehavior); } var title = this.title; diff --git a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart index ead1d8f08c..fcb1777ebe 100644 --- a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart @@ -56,9 +56,13 @@ class _StreamThreadPageState extends State { @override Widget build(BuildContext context) { + final appBar = StreamThreadHeader(parent: widget.parent); + final isFloating = StreamTheme.of(context).appStyle.appBarBehavior == .floating; + return Scaffold( backgroundColor: context.streamColorScheme.backgroundApp, - appBar: StreamThreadHeader(parent: widget.parent), + appBar: appBar, + extendBodyBehindAppBar: isFloating, body: Column( children: [ Expanded( @@ -73,6 +77,7 @@ class _StreamThreadPageState extends State { highlightInitialMessage: true, ), onViewInChannelTap: widget.onViewInChannelTap, + topPadding: isFloating ? appBar.preferredSize.height + MediaQuery.of(context).padding.top : 0.0, ), ), if (widget.parent.type != 'deleted') diff --git a/packages/stream_chat_flutter/lib/src/misc/back_button.dart b/packages/stream_chat_flutter/lib/src/misc/back_button.dart index 2910fc903e..a649ccdcb1 100644 --- a/packages/stream_chat_flutter/lib/src/misc/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/misc/back_button.dart @@ -11,7 +11,7 @@ class StreamBackButton extends StatelessWidget { this.onPressed, this.showUnreadCount = false, this.channelId, - this.floating = false, + this.appBarBehavior, }); /// Callback for when button is pressed @@ -24,7 +24,7 @@ class StreamBackButton extends StatelessWidget { final String? channelId; /// Whether the back button is floating. - final bool floating; + final AppBarBehavior? appBarBehavior; @override Widget build(BuildContext context) { @@ -32,10 +32,14 @@ class StreamBackButton extends StatelessWidget { .iOS || .macOS => context.streamIcons.chevronLeft, _ => context.streamIcons.arrowLeft, }; + final isFloating = switch (appBarBehavior ?? StreamTheme.of(context).appStyle.appBarBehavior) { + .floating => true, + .regular => false, + }; Widget button = StreamButton.icon( - type: floating ? .outline : .ghost, - isFloating: floating, + type: isFloating ? .outline : .ghost, + isFloating: isFloating, size: .medium, style: .secondary, icon: Icon(iconData), diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index a3184b341f..b58f5d6371 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -3,6 +3,9 @@ export 'package:photo_manager/photo_manager.dart' show ThumbnailSize, ThumbnailF export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'package:stream_core_flutter/stream_core_flutter.dart' show + StreamAppStyle, + ComposerLocation, + AppBarBehavior, StreamAppBar, StreamAppBarProps, StreamAppBarStyle, diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index ba2fc8c5d8..de507c91e6 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -173,11 +173,19 @@ class _StreamChatSampleAppState extends State return MaterialApp.router( theme: ThemeData( brightness: .light, - extensions: [StreamTheme.light()], + extensions: [ + StreamTheme.light().copyWith( + appStyle: const StreamAppStyle.floating(), + ), + ], ), darkTheme: ThemeData( brightness: .dark, - extensions: [StreamTheme.dark()], + extensions: [ + StreamTheme.dark().copyWith( + appStyle: const StreamAppStyle.floating(), + ), + ], ), themeMode: config.themeMode, locale: config.locale, From 558f07376a1b8203ddacf45fc65d499c7eb3bb66 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 29 May 2026 09:21:45 +0200 Subject: [PATCH 14/48] Use streamscaffold --- .../lib/src/channel/channel_page.dart | 203 +++++------------ .../lib/src/channel/thread_page.dart | 79 ++++--- .../lib/stream_chat_flutter.dart | 3 + sample_app/lib/pages/channel_list_page.dart | 205 +++++++++++++++--- sample_app/lib/widgets/channel_list.dart | 24 +- 5 files changed, 305 insertions(+), 209 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index c6f438e1d5..48e21105f4 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -1,5 +1,3 @@ -// ignore_for_file: deprecated_member_use, avoid_redundant_argument_values - import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -66,14 +64,8 @@ class _StreamChannelPageState extends State { @override Widget build(BuildContext context) { - final typingIndicator = StreamTypingIndicator( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - style: context.streamTextTheme.captionDefault.copyWith( - color: context.streamColorScheme.textSecondary, - ), + final appBar = StreamChannelHeader( + onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), ); final composer = StreamMessageComposer( @@ -83,161 +75,82 @@ class _StreamChannelPageState extends State { enableVoiceRecording: true, ); - final isFloating = StreamTheme.of(context).appStyle.appBarBehavior == .floating; - - final appBar = StreamChannelHeader( - onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), - ); - - final topPadding = isFloating ? appBar.preferredSize.height + MediaQuery.of(context).padding.top : 0.0; - - StreamMessageListView messageListBuilder(double bottomPadding) => StreamMessageListView( - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - config: StreamMessageListViewConfiguration( - highlightInitialMessage: widget.highlightInitialMessage, - swipeToReply: true, - ), - onEditMessageTap: _editMessage, - onReplyTap: _reply, - threadBuilder: (_, parentMessage) { - return StreamThreadPage(parent: parentMessage!); - }, - topPadding: topPadding, - bottomPadding: bottomPadding, - ); - - final body = switch (isFloating) { - true => _FloatingChannelBody( - composer: composer, - typingIndicator: typingIndicator, - messageListBuilder: messageListBuilder, + final typingIndicator = StreamTypingIndicator( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, ), - false => Column( - children: [ - Expanded( - child: Stack( - children: [ - messageListBuilder(0), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: typingIndicator, - ), - ], - ), - ), - composer, - ], + style: context.streamTextTheme.captionDefault.copyWith( + color: context.streamColorScheme.textSecondary, ), - }; + ); - return Scaffold( + return StreamScaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: appBar, - extendBodyBehindAppBar: isFloating, - body: body, - ); - } -} - -// --------------------------------------------------------------------------- -// Floating layout helpers -// --------------------------------------------------------------------------- - -/// Layout slot identifiers for [_FloatingChannelBodyDelegate]. -enum _Slot { body, typing, composer } - -/// A custom [BoxConstraints] subclass that carries the composer height so that -/// a [LayoutBuilder] inside the body slot can read it without a separate -/// callback or [setState]. -/// -/// The [==] / [hashCode] overrides are intentional: they make the framework -/// re-run [RenderObject.performLayout] on the body child even when the outer -/// size constraints have not changed but only [composerHeight] has. -class _BodyBoxConstraints extends BoxConstraints { - const _BodyBoxConstraints({ - super.maxWidth, - super.maxHeight, - required this.composerHeight, - }) : assert(composerHeight >= 0, 'composerHeight must be non-negative'); - - /// Height of the floating composer, used as the list's bottom inset. - final double composerHeight; - - @override - bool operator ==(Object other) { - if (super != other) return false; - return other is _BodyBoxConstraints && other.composerHeight == composerHeight; - } - - @override - int get hashCode => Object.hash(super.hashCode, composerHeight); -} - -/// [MultiChildLayoutDelegate] that measures the composer first, then lays out -/// the typing indicator and body in the same layout pass — no post-frame -/// callback required. -class _FloatingChannelBodyDelegate extends MultiChildLayoutDelegate { - @override - void performLayout(Size size) { - // 1. Measure + position the composer at the bottom. - final composerSize = layoutChild(_Slot.composer, BoxConstraints.loose(size)); - positionChild(_Slot.composer, Offset(0, size.height - composerSize.height)); - - // 2. Measure + position the typing indicator just above the composer. - final typingSize = layoutChild(_Slot.typing, BoxConstraints(maxWidth: size.width)); - positionChild(_Slot.typing, Offset(0, size.height - composerSize.height - typingSize.height)); - - // 3. Lay out the body with the composer height embedded in the constraints - // so the LayoutBuilder inside can read it synchronously. - layoutChild( - _Slot.body, - _BodyBoxConstraints( - maxWidth: size.width, - maxHeight: size.height, - composerHeight: composerSize.height, + bottom: composer, + body: _ChannelPageBody( + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + highlightInitialMessage: widget.highlightInitialMessage, + onReply: _reply, + onEditMessage: _editMessage, + typingIndicator: typingIndicator, ), ); - positionChild(_Slot.body, Offset.zero); } - - @override - bool shouldRelayout(_FloatingChannelBodyDelegate oldDelegate) => false; } -/// Stateless widget that wires the floating layout together. -class _FloatingChannelBody extends StatelessWidget { - const _FloatingChannelBody({ - required this.composer, +/// 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.messageListBuilder, + required this.onReply, + required this.onEditMessage, + this.initialScrollIndex, + this.initialAlignment, + this.highlightInitialMessage = false, }); - final Widget composer; final Widget typingIndicator; - - /// Called with the current composer height so the list can set its inset. - final Widget Function(double bottomWidgetsHeight) messageListBuilder; + final void Function(Message) onReply; + final void Function(Message) onEditMessage; + final int? initialScrollIndex; + final double? initialAlignment; + final bool highlightInitialMessage; @override Widget build(BuildContext context) { - return CustomMultiChildLayout( - delegate: _FloatingChannelBodyDelegate(), + final insets = StreamScaffoldInsets.of(context); + + return Stack( children: [ - // Body slot: a LayoutBuilder that reads the custom constraints. - LayoutId( - id: _Slot.body, - child: LayoutBuilder( - builder: (context, constraints) { - final height = constraints is _BodyBoxConstraints ? constraints.composerHeight : 0.0; - return messageListBuilder(height); - }, + StreamMessageListView( + initialScrollIndex: initialScrollIndex, + initialAlignment: initialAlignment, + config: StreamMessageListViewConfiguration( + highlightInitialMessage: highlightInitialMessage, + swipeToReply: true, ), + 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, ), - LayoutId(id: _Slot.typing, child: typingIndicator), - LayoutId(id: _Slot.composer, child: composer), ], ); } diff --git a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart index fcb1777ebe..aeaf40523f 100644 --- a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart @@ -57,37 +57,62 @@ class _StreamThreadPageState extends State { @override Widget build(BuildContext context) { final appBar = StreamThreadHeader(parent: widget.parent); - final isFloating = StreamTheme.of(context).appStyle.appBarBehavior == .floating; - return Scaffold( + final composer = widget.parent.type != 'deleted' + ? StreamMessageComposer( + focusNode: _focusNode, + messageComposerController: _messageComposerController, + enableVoiceRecording: true, + ) + : null; + + return StreamScaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: appBar, - extendBodyBehindAppBar: isFloating, - body: Column( - children: [ - Expanded( - child: StreamMessageListView( - parentMessage: widget.parent, - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - onReplyTap: _reply, - config: const StreamMessageListViewConfiguration( - swipeToReply: true, - showScrollToBottom: false, - highlightInitialMessage: true, - ), - onViewInChannelTap: widget.onViewInChannelTap, - topPadding: isFloating ? appBar.preferredSize.height + MediaQuery.of(context).padding.top : 0.0, - ), - ), - if (widget.parent.type != 'deleted') - StreamMessageComposer( - focusNode: _focusNode, - messageComposerController: _messageComposerController, - enableVoiceRecording: true, - ), - ], + bottom: composer, + body: _ThreadBody( + parent: widget.parent, + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + onReply: _reply, + onViewInChannelTap: widget.onViewInChannelTap, + ), + ); + } +} + +class _ThreadBody extends StatelessWidget { + const _ThreadBody({ + required this.parent, + required this.onReply, + this.initialScrollIndex, + this.initialAlignment, + this.onViewInChannelTap, + }); + + final Message parent; + final void Function(Message) onReply; + final int? initialScrollIndex; + final double? initialAlignment; + final void Function(Message message)? onViewInChannelTap; + + @override + Widget build(BuildContext context) { + final insets = StreamScaffoldInsets.of(context); + + return StreamMessageListView( + parentMessage: parent, + initialScrollIndex: initialScrollIndex, + initialAlignment: initialAlignment, + onReplyTap: onReply, + config: const StreamMessageListViewConfiguration( + swipeToReply: true, + showScrollToBottom: false, + highlightInitialMessage: true, ), + onViewInChannelTap: onViewInChannelTap, + topPadding: insets.topPadding, + bottomPadding: insets.bottomPadding, ); } } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index b58f5d6371..208846f2a5 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -6,6 +6,9 @@ export 'package:stream_core_flutter/stream_core_flutter.dart' StreamAppStyle, ComposerLocation, AppBarBehavior, + BottomBarBehavior, + StreamScaffold, + StreamScaffoldInsets, StreamAppBar, StreamAppBarProps, StreamAppBarStyle, diff --git a/sample_app/lib/pages/channel_list_page.dart b/sample_app/lib/pages/channel_list_page.dart index 4b7cd032a3..6eb99de8df 100644 --- a/sample_app/lib/pages/channel_list_page.dart +++ b/sample_app/lib/pages/channel_list_page.dart @@ -1,4 +1,4 @@ -// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use, avoid_redundant_argument_values import 'dart:async'; @@ -74,40 +74,27 @@ class _ChannelListPageState extends State { final enabledTabs = allTabs.where((t) => t.enabled).toList(); - return Scaffold( + final isFloating = StreamTheme.of(context).appStyle.bottomBarBehavior == BottomBarBehavior.floating; + + final bottomNav = isFloating + ? _FloatingTabBar( + currentIndex: _currentIndex, + tabs: enabledTabs, + onTap: (i) => setState(() => _currentIndex = i), + ) + : _RegularBottomNavBar( + currentIndex: _currentIndex, + tabs: enabledTabs, + onTap: (i) => setState(() => _currentIndex = i), + ); + + return StreamScaffold( backgroundColor: colorScheme.backgroundApp, appBar: StreamChannelListHeader( title: Text(enabledTabs[_currentIndex].label, style: textTheme.headingSm), ), drawer: LeftDrawer(user: user), - bottomNavigationBar: DecoratedBox( - decoration: BoxDecoration( - color: colorScheme.backgroundElevation1, - border: Border(top: BorderSide(color: colorScheme.borderSubtle)), - ), - child: StreamBadgeNotificationTheme( - data: const .new(size: .xs), - child: BottomNavigationBar( - elevation: 0, - iconSize: 20, - currentIndex: _currentIndex, - type: BottomNavigationBarType.fixed, - selectedItemColor: colorScheme.textPrimary, - unselectedItemColor: colorScheme.textTertiary, - backgroundColor: Colors.transparent, - selectedLabelStyle: textTheme.metadataEmphasis, - unselectedLabelStyle: textTheme.metadataEmphasis, - onTap: (index) => setState(() => _currentIndex = index), - items: enabledTabs.map((tab) { - return BottomNavigationBarItem( - icon: tab.icon, - activeIcon: tab.selectedIcon, - label: tab.label, - ); - }).toList(), - ), - ), - ), + bottom: bottomNav, body: IndexedStack( index: _currentIndex, children: [for (final tab in enabledTabs) tab.page], @@ -141,6 +128,164 @@ class _ChannelListPageState extends State { } } +// --------------------------------------------------------------------------- +// Floating pill tab bar (floating bottom bar behavior) +// --------------------------------------------------------------------------- + +class _FloatingTabBar extends StatelessWidget { + const _FloatingTabBar({ + required this.currentIndex, + required this.tabs, + required this.onTap, + }); + + final int currentIndex; + final List<_TabDef> tabs; + final ValueChanged onTap; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + final radius = context.streamRadius; + + return SafeArea( + top: false, + child: Padding( + padding: EdgeInsets.only( + left: spacing.xl, + right: spacing.xl, + ), + child: Container( + decoration: BoxDecoration( + color: colorScheme.backgroundElevation1, + borderRadius: BorderRadius.all(radius.max), + boxShadow: context.streamBoxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.max), + border: Border.all(color: colorScheme.borderSubtle), + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: spacing.xs), + child: StreamBadgeNotificationTheme( + data: const .new(size: .xs), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + for (var i = 0; i < tabs.length; i++) + _FloatingTabItem( + tab: tabs[i], + selected: i == currentIndex, + onTap: () => onTap(i), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _FloatingTabItem extends StatelessWidget { + const _FloatingTabItem({ + required this.tab, + required this.selected, + required this.onTap, + }); + + final _TabDef tab; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + final color = selected ? colorScheme.textPrimary : colorScheme.textTertiary; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: spacing.md, vertical: spacing.xxs), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: spacing.xxxs, + children: [ + IconTheme( + data: IconThemeData(color: color, size: 20), + child: selected ? tab.selectedIcon : tab.icon, + ), + Text( + tab.label, + style: textTheme.metadataEmphasis.copyWith(color: color), + ), + ], + ), + ), + ); + } +} + +// --------------------------------------------------------------------------- +// Regular (docked) bottom navigation bar +// --------------------------------------------------------------------------- + +class _RegularBottomNavBar extends StatelessWidget { + const _RegularBottomNavBar({ + required this.currentIndex, + required this.tabs, + required this.onTap, + }); + + final int currentIndex; + final List<_TabDef> tabs; + final ValueChanged onTap; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + + return DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.backgroundElevation1, + border: Border(top: BorderSide(color: colorScheme.borderSubtle)), + ), + child: StreamBadgeNotificationTheme( + data: const .new(size: .xs), + child: BottomNavigationBar( + elevation: 0, + iconSize: 20, + currentIndex: currentIndex, + type: BottomNavigationBarType.fixed, + selectedItemColor: colorScheme.textPrimary, + unselectedItemColor: colorScheme.textTertiary, + backgroundColor: Colors.transparent, + selectedLabelStyle: textTheme.metadataEmphasis, + unselectedLabelStyle: textTheme.metadataEmphasis, + onTap: onTap, + items: tabs.map((tab) { + return BottomNavigationBarItem( + icon: tab.icon, + activeIcon: tab.selectedIcon, + label: tab.label, + ); + }).toList(), + ), + ), + ); + } +} + +// --------------------------------------------------------------------------- +// Tab definition +// --------------------------------------------------------------------------- + class _TabDef { const _TabDef({ required this.icon, diff --git a/sample_app/lib/widgets/channel_list.dart b/sample_app/lib/widgets/channel_list.dart index c32abfd2e5..ed3191e852 100644 --- a/sample_app/lib/widgets/channel_list.dart +++ b/sample_app/lib/widgets/channel_list.dart @@ -89,14 +89,21 @@ class _ChannelList extends State { }, child: NestedScrollView( controller: _scrollController, - headerSliverBuilder: (_, __) => [ - SliverToBoxAdapter( - child: SearchTextField( - controller: _controller, - hintText: 'Search', + headerSliverBuilder: (context, __) { + // When the app bar is floating it overlaps this scroll view from + // the top. Insert a spacer sliver so the search bar starts below + // the visible bottom edge of the floating bar. + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; + return [ + if (topInset > 0) SliverToBoxAdapter(child: SizedBox(height: topInset)), + SliverToBoxAdapter( + child: SearchTextField( + controller: _controller, + hintText: 'Search', + ), ), - ), - ], + ]; + }, body: _isSearchActive ? _ChannelListSearch(_messageSearchListController) : _ChannelListDefault(_channelListController), @@ -113,11 +120,14 @@ class _ChannelListDefault extends StatelessWidget { @override Widget build(BuildContext context) { + final bottomPadding = StreamScaffoldInsets.maybeOf(context)?.bottomPadding ?? 0.0; + return SlidableAutoCloseBehavior( child: RefreshIndicator( onRefresh: channelListController.refresh, child: StreamChannelListView( controller: channelListController, + padding: bottomPadding > 0 ? EdgeInsets.only(bottom: bottomPadding) : null, itemBuilder: (context, channels, index, defaultWidget) { final channel = channels[index]; From 1ff997d1fd3d38197b274d9d5704af0d62fc3803 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 29 May 2026 13:15:37 +0200 Subject: [PATCH 15/48] migrate to SDK navbar --- .../lib/stream_chat_flutter.dart | 2 + sample_app/lib/pages/channel_list_page.dart | 216 +++--------------- 2 files changed, 30 insertions(+), 188 deletions(-) diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 208846f2a5..03f5ded2d1 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -25,6 +25,8 @@ export 'package:stream_core_flutter/stream_core_flutter.dart' StreamBottomAppBarStyle, StreamBottomAppBarTheme, StreamBottomAppBarThemeData, + StreamBottomNavBar, + StreamBottomNavBarItem, StreamButton, StreamButtonSize, StreamButtonStyle, diff --git a/sample_app/lib/pages/channel_list_page.dart b/sample_app/lib/pages/channel_list_page.dart index 6eb99de8df..98c5a6309e 100644 --- a/sample_app/lib/pages/channel_list_page.dart +++ b/sample_app/lib/pages/channel_list_page.dart @@ -45,28 +45,36 @@ class _ChannelListPageState extends State { final allTabs = <_TabDef>[ _TabDef( - icon: StreamUnreadIndicator(child: Icon(icons.messageBubble)), - selectedIcon: StreamUnreadIndicator(child: Icon(icons.messageBubbleFill)), - label: 'Chats', + navItem: StreamBottomNavBarItem( + icon: StreamUnreadIndicator(child: Icon(icons.messageBubble)), + selectedIcon: StreamUnreadIndicator(child: Icon(icons.messageBubbleFill)), + label: 'Chats', + ), page: const ChannelList(), ), _TabDef( - icon: StreamUnreadIndicator.threads(child: Icon(icons.thread)), - selectedIcon: StreamUnreadIndicator.threads(child: Icon(icons.threadFill)), - label: 'Threads', + navItem: StreamBottomNavBarItem( + icon: StreamUnreadIndicator.threads(child: Icon(icons.thread)), + selectedIcon: StreamUnreadIndicator.threads(child: Icon(icons.threadFill)), + label: 'Threads', + ), page: const ThreadListPage(), ), _TabDef( - icon: const Icon(Icons.drafts_outlined), - selectedIcon: const Icon(Icons.drafts_rounded), - label: 'Drafts', + navItem: const StreamBottomNavBarItem( + icon: Icon(Icons.drafts_outlined), + selectedIcon: Icon(Icons.drafts_rounded), + label: 'Drafts', + ), page: const DraftListPage(), enabled: config.draftMessagesEnabled, ), _TabDef( - icon: const Icon(Icons.bookmark_outline_rounded), - selectedIcon: const Icon(Icons.bookmark_rounded), - label: 'Reminders', + navItem: const StreamBottomNavBarItem( + icon: Icon(Icons.bookmark_outline_rounded), + selectedIcon: Icon(Icons.bookmark_rounded), + label: 'Reminders', + ), page: const RemindersPage(), enabled: config.enableReminderActions, ), @@ -74,27 +82,17 @@ class _ChannelListPageState extends State { final enabledTabs = allTabs.where((t) => t.enabled).toList(); - final isFloating = StreamTheme.of(context).appStyle.bottomBarBehavior == BottomBarBehavior.floating; - - final bottomNav = isFloating - ? _FloatingTabBar( - currentIndex: _currentIndex, - tabs: enabledTabs, - onTap: (i) => setState(() => _currentIndex = i), - ) - : _RegularBottomNavBar( - currentIndex: _currentIndex, - tabs: enabledTabs, - onTap: (i) => setState(() => _currentIndex = i), - ); - return StreamScaffold( backgroundColor: colorScheme.backgroundApp, appBar: StreamChannelListHeader( - title: Text(enabledTabs[_currentIndex].label, style: textTheme.headingSm), + title: Text(enabledTabs[_currentIndex].navItem.label, style: textTheme.headingSm), ), drawer: LeftDrawer(user: user), - bottom: bottomNav, + bottom: StreamBottomNavBar( + currentIndex: _currentIndex, + onTap: (i) => setState(() => _currentIndex = i), + items: [for (final tab in enabledTabs) tab.navItem], + ), body: IndexedStack( index: _currentIndex, children: [for (final tab in enabledTabs) tab.page], @@ -128,176 +126,18 @@ class _ChannelListPageState extends State { } } -// --------------------------------------------------------------------------- -// Floating pill tab bar (floating bottom bar behavior) -// --------------------------------------------------------------------------- - -class _FloatingTabBar extends StatelessWidget { - const _FloatingTabBar({ - required this.currentIndex, - required this.tabs, - required this.onTap, - }); - - final int currentIndex; - final List<_TabDef> tabs; - final ValueChanged onTap; - - @override - Widget build(BuildContext context) { - final colorScheme = context.streamColorScheme; - final spacing = context.streamSpacing; - final radius = context.streamRadius; - - return SafeArea( - top: false, - child: Padding( - padding: EdgeInsets.only( - left: spacing.xl, - right: spacing.xl, - ), - child: Container( - decoration: BoxDecoration( - color: colorScheme.backgroundElevation1, - borderRadius: BorderRadius.all(radius.max), - boxShadow: context.streamBoxShadow.elevation1, - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.all(radius.max), - border: Border.all(color: colorScheme.borderSubtle), - ), - child: Padding( - padding: EdgeInsets.symmetric(vertical: spacing.xs), - child: StreamBadgeNotificationTheme( - data: const .new(size: .xs), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - for (var i = 0; i < tabs.length; i++) - _FloatingTabItem( - tab: tabs[i], - selected: i == currentIndex, - onTap: () => onTap(i), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class _FloatingTabItem extends StatelessWidget { - const _FloatingTabItem({ - required this.tab, - required this.selected, - required this.onTap, - }); - - final _TabDef tab; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final textTheme = context.streamTextTheme; - final colorScheme = context.streamColorScheme; - final spacing = context.streamSpacing; - final color = selected ? colorScheme.textPrimary : colorScheme.textTertiary; - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: spacing.md, vertical: spacing.xxs), - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: spacing.xxxs, - children: [ - IconTheme( - data: IconThemeData(color: color, size: 20), - child: selected ? tab.selectedIcon : tab.icon, - ), - Text( - tab.label, - style: textTheme.metadataEmphasis.copyWith(color: color), - ), - ], - ), - ), - ); - } -} - -// --------------------------------------------------------------------------- -// Regular (docked) bottom navigation bar -// --------------------------------------------------------------------------- - -class _RegularBottomNavBar extends StatelessWidget { - const _RegularBottomNavBar({ - required this.currentIndex, - required this.tabs, - required this.onTap, - }); - - final int currentIndex; - final List<_TabDef> tabs; - final ValueChanged onTap; - - @override - Widget build(BuildContext context) { - final colorScheme = context.streamColorScheme; - final textTheme = context.streamTextTheme; - - return DecoratedBox( - decoration: BoxDecoration( - color: colorScheme.backgroundElevation1, - border: Border(top: BorderSide(color: colorScheme.borderSubtle)), - ), - child: StreamBadgeNotificationTheme( - data: const .new(size: .xs), - child: BottomNavigationBar( - elevation: 0, - iconSize: 20, - currentIndex: currentIndex, - type: BottomNavigationBarType.fixed, - selectedItemColor: colorScheme.textPrimary, - unselectedItemColor: colorScheme.textTertiary, - backgroundColor: Colors.transparent, - selectedLabelStyle: textTheme.metadataEmphasis, - unselectedLabelStyle: textTheme.metadataEmphasis, - onTap: onTap, - items: tabs.map((tab) { - return BottomNavigationBarItem( - icon: tab.icon, - activeIcon: tab.selectedIcon, - label: tab.label, - ); - }).toList(), - ), - ), - ); - } -} - // --------------------------------------------------------------------------- // Tab definition // --------------------------------------------------------------------------- class _TabDef { const _TabDef({ - required this.icon, - required this.selectedIcon, - required this.label, + required this.navItem, required this.page, this.enabled = true, }); - final Widget icon; - final Widget selectedIcon; - final String label; + final StreamBottomNavBarItem navItem; final Widget page; final bool enabled; } From 4bd8056662fb248a28052e9a2f10114813002d38 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 10 Jun 2026 12:20:48 +0200 Subject: [PATCH 16/48] fix edit message in thread broken due to merge --- .../lib/src/channel/thread_page.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart index aeaf40523f..a68d4cfdd3 100644 --- a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart @@ -54,6 +54,13 @@ class _StreamThreadPageState extends State { }); } + void _editMessage(Message message) { + _messageComposerController.editMessage(message); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _focusNode.requestFocus(); + }); + } + @override Widget build(BuildContext context) { final appBar = StreamThreadHeader(parent: widget.parent); @@ -67,7 +74,6 @@ class _StreamThreadPageState extends State { : null; return StreamScaffold( - backgroundColor: context.streamColorScheme.backgroundApp, appBar: appBar, bottom: composer, body: _ThreadBody( @@ -75,6 +81,7 @@ class _StreamThreadPageState extends State { initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, onReply: _reply, + onEditMessageTap: _editMessage, onViewInChannelTap: widget.onViewInChannelTap, ), ); @@ -85,6 +92,7 @@ class _ThreadBody extends StatelessWidget { const _ThreadBody({ required this.parent, required this.onReply, + required this.onEditMessageTap, this.initialScrollIndex, this.initialAlignment, this.onViewInChannelTap, @@ -95,6 +103,7 @@ class _ThreadBody extends StatelessWidget { final int? initialScrollIndex; final double? initialAlignment; final void Function(Message message)? onViewInChannelTap; + final void Function(Message message)? onEditMessageTap; @override Widget build(BuildContext context) { @@ -105,6 +114,7 @@ class _ThreadBody extends StatelessWidget { initialScrollIndex: initialScrollIndex, initialAlignment: initialAlignment, onReplyTap: onReply, + onEditMessageTap: onEditMessageTap, config: const StreamMessageListViewConfiguration( swipeToReply: true, showScrollToBottom: false, From 84e85ce4cd515b588c2fd70189dfb25b1ee876e8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 11 Jun 2026 16:46:47 +0200 Subject: [PATCH 17/48] Add shadow to avatar on channel header --- .../lib/src/channel/channel_header.dart | 15 +++++++++++++-- .../components/avatar/stream_channel_avatar.dart | 11 +++++++++++ .../src/components/avatar/stream_user_avatar.dart | 9 +++++++++ .../avatar/stream_user_avatar_group.dart | 9 +++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index ccc4cfb49d..89d7aaac20 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -153,8 +153,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( @@ -203,10 +212,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) { @@ -227,6 +237,7 @@ class _DefaultChannelAvatar extends StatelessWidget { child: StreamChannelAvatar( size: .lg, channel: channel, + showShadow: showShadow, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart index 92739aa219..8ac6723233 100644 --- a/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart @@ -54,6 +54,7 @@ class StreamChannelAvatar extends StatelessWidget { super.key, this.size, required this.channel, + this.showShadow = false, }); /// The channel whose avatar is displayed. @@ -64,6 +65,13 @@ class StreamChannelAvatar extends StatelessWidget { /// If null, defaults to [StreamAvatarGroupSize.lg]. final StreamAvatarGroupSize? size; + /// Whether to show a drop shadow around the avatar. + /// + /// Defaults to false. The shadow style is determined by + /// [StreamAvatarThemeData.boxShadow], falling back to + /// [StreamBoxShadow.elevation3]. + final bool showShadow; + @override Widget build(BuildContext context) { assert(channel.state != null, 'Channel ${channel.id} is not initialized'); @@ -76,6 +84,7 @@ class StreamChannelAvatar extends StatelessWidget { builder: (context, channelImage) => StreamAvatar( imageUrl: channelImage, size: _avatarSizeForAvatarGroupSize(effectiveSize), + showShadow: showShadow, placeholder: (_) => const _StreamChannelAvatarPlaceholder(), ), noDataBuilder: (context) => BetterStreamBuilder( @@ -95,12 +104,14 @@ class StreamChannelAvatar extends StatelessWidget { size: _avatarSizeForAvatarGroupSize(effectiveSize), // TODO: make this configurable when the online state is shown. showOnlineIndicator: otherUser.online, + showShadow: showShadow, ); } return StreamUserAvatarGroup( size: effectiveSize, users: users.sortedBy((it) => it.id == currentUserId ? 1 : 0), + showShadow: showShadow, ); }, ), diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart index 81ad91a98a..747ae2b5f0 100644 --- a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart @@ -68,6 +68,7 @@ class StreamUserAvatar extends StatelessWidget { required this.user, this.showBorder = true, this.showOnlineIndicator = true, + this.showShadow = false, }); /// The user whose avatar is displayed. @@ -84,6 +85,13 @@ class StreamUserAvatar extends StatelessWidget { /// Defaults to true. final bool showOnlineIndicator; + /// Whether to show a drop shadow around the avatar. + /// + /// Defaults to false. The shadow style is determined by + /// [StreamAvatarThemeData.boxShadow], falling back to + /// [StreamBoxShadow.elevation3]. + final bool showShadow; + /// The size of the avatar. /// /// If null, uses [StreamAvatarThemeData.size], or falls back to @@ -107,6 +115,7 @@ class StreamUserAvatar extends StatelessWidget { size: effectiveSize, imageUrl: user.image, showBorder: showBorder, + showShadow: showShadow, backgroundColor: effectiveBackgroundColor, foregroundColor: effectiveForegroundColor, placeholder: (_) => _StreamUserAvatarPlaceholder(user: user, size: effectiveSize), diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart index 5b557f4e6b..aebd47c9ae 100644 --- a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart @@ -55,6 +55,7 @@ class StreamUserAvatarGroup extends StatelessWidget { super.key, required this.users, this.size, + this.showShadow = false, }); /// The list of users whose avatars are displayed. @@ -65,6 +66,13 @@ class StreamUserAvatarGroup extends StatelessWidget { /// If null, defaults to [StreamAvatarGroupSize.lg]. final StreamAvatarGroupSize? size; + /// Whether to show a drop shadow around the avatar group. + /// + /// Defaults to false. The shadow style is determined by + /// [StreamAvatarThemeData.boxShadow], falling back to + /// [StreamBoxShadow.elevation3]. + final bool showShadow; + @override Widget build(BuildContext context) { return StreamAvatarGroup( @@ -73,6 +81,7 @@ class StreamUserAvatarGroup extends StatelessWidget { (user) => StreamUserAvatar( user: user, showOnlineIndicator: false, + showShadow: showShadow, ), ), ); From c27569b3761ba3ec1c0e9c965da861503986de18 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 09:35:37 +0200 Subject: [PATCH 18/48] Add gradient behind floating composer --- .../lib/src/channel/channel_list_header.dart | 15 +- .../stream_message_composer.dart | 218 ++++++++++++------ 2 files changed, 155 insertions(+), 78 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart index e4ed899c7b..4e9198bde2 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart @@ -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. @@ -117,6 +118,9 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi /// [StreamChatThemeData.channelListHeaderTheme]. final StreamAppBarStyle? style; + /// Whether the header is floating. + final AppBarBehavior? appBarBehavior; + @override Size get preferredSize => const Size.fromHeight(kStreamToolbarHeight); @@ -125,7 +129,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( @@ -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.showShadow = false}); final StreamChatClient client; final void Function(User user)? onPressed; + final bool showShadow; @override Widget build(BuildContext context) { @@ -211,6 +221,7 @@ class _DefaultUserAvatar extends StatelessWidget { size: .lg, user: user, showOnlineIndicator: false, + showShadow: showShadow, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index c8428f1e9a..180a7c1cc6 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -7,11 +7,13 @@ import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/message_input/error_alert_sheet.dart'; import 'package:stream_chat_flutter/src/message_input/stream_chat_message_input.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_core_flutter/stream_core_flutter.dart'; const _kCommandTrigger = '/'; const _kMentionTrigger = '@'; +/// Fixed height of the inline attachment picker body. +const _kPickerBodyHeight = 333.0; + /// Signature for the function that determines if a [matchedUri] should be /// previewed as an OG Attachment. typedef OgPreviewFilter = bool Function(Uri matchedUri, String messageText); @@ -797,34 +799,56 @@ class DefaultStreamMessageComposerState extends State 0) { + final bandColor = context.streamColorScheme.backgroundElevation1; + return Stack( + children: [ + Positioned( + bottom: 0, + left: 0, + right: 0, + height: safeAreaPadding.bottom, + child: ColoredBox(color: bandColor), + ), + Padding(padding: safeAreaPadding, child: child), + ], + ); + } + + return Padding(padding: safeAreaPadding, child: child); + }, + child: Center(heightFactor: 1, child: messageInput), + ); + return Material( color: Colors.transparent, - child: DecoratedBox( - decoration: BoxDecoration( - color: switch (effectiveComposerLocation) { - .floating => null, - .docked => context.streamColorScheme.backgroundElevation1, - }, + child: switch (effectiveComposerLocation) { + .floating => composerBody, + .docked => DecoratedBox( + decoration: BoxDecoration(color: context.streamColorScheme.backgroundElevation1), + child: composerBody, ), - child: AnimatedBuilder( - animation: _pickerAnimation, - builder: (context, child) { - final safeAreaPadding = safeAreaEnabled - ? EdgeInsets.lerp( - EdgeInsets.only( - left: viewPadding.left, - right: viewPadding.right, - bottom: math.max(viewPadding.bottom, spacing.md), - ), - EdgeInsets.zero, - _pickerAnimation.value, - )! - : EdgeInsets.zero; - return Padding(padding: safeAreaPadding, child: child); - }, - child: Center(heightFactor: 1, child: messageInput), - ), - ), + }, ); } @@ -893,64 +917,106 @@ class DefaultStreamMessageComposerState extends State PopScope( - canPop: !_isPickerVisible, - onPopInvokedWithResult: (didPop, _) { - if (!didPop) _hidePicker(); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - DropTarget( - onDragDone: (details) async { - final attachments = []; - for (final file in details.files) { - attachments.add(await file.toAttachment(type: AttachmentType.file)); - } - if (attachments.isNotEmpty) _addAttachments(attachments); + builder: (context, value, _) { + // Extracted so the floating gradient can wrap just the pill, keeping + // gradient height stable when the picker (a sibling) opens. + final pill = DropTarget( + onDragDone: (details) async { + final attachments = []; + for (final file in details.files) { + attachments.add(await file.toAttachment(type: AttachmentType.file)); + } + if (attachments.isNotEmpty) _addAttachments(attachments); + }, + onDragEntered: (_) {}, + onDragExited: (_) {}, + child: Focus( + skipTraversal: true, + onKeyEvent: _handleKeyPressed, + child: StreamChatMessageInput( + controller: controller, + currentUserId: currentUserId, + onAttachmentButtonPressed: widget.props.disableAttachments ? null : _onAttachmentButtonPressed, + isPickerOpen: _isPickerVisible, + placeholder: _buildPlaceholder(context), + focusNode: focusNode, + onSendPressed: sendMessage, + canAlsoSendToChannel: _shouldShowSendToChannelCheckbox(), + audioRecorderController: widget.props.enableVoiceRecording ? _audioRecorderController : null, + sendVoiceRecordingAutomatically: widget.props.sendVoiceRecordingAutomatically, + feedback: widget.props.voiceRecordingFeedback, + onQuotedMessageCleared: () { + _effectiveController.clearQuotedMessage(); + widget.props.onQuotedMessageCleared?.call(); }, - onDragEntered: (_) {}, - onDragExited: (_) {}, - child: Focus( - skipTraversal: true, - onKeyEvent: _handleKeyPressed, - child: StreamChatMessageInput( - controller: controller, - currentUserId: currentUserId, - onAttachmentButtonPressed: widget.props.disableAttachments ? null : _onAttachmentButtonPressed, - isPickerOpen: _isPickerVisible, - placeholder: _buildPlaceholder(context), - focusNode: focusNode, - onSendPressed: sendMessage, - canAlsoSendToChannel: _shouldShowSendToChannelCheckbox(), - audioRecorderController: widget.props.enableVoiceRecording ? _audioRecorderController : null, - sendVoiceRecordingAutomatically: widget.props.sendVoiceRecordingAutomatically, - feedback: widget.props.voiceRecordingFeedback, - onQuotedMessageCleared: () { - _effectiveController.clearQuotedMessage(); - widget.props.onQuotedMessageCleared?.call(); - }, - textInputAction: widget.props.textInputAction, - keyboardType: widget.props.keyboardType, - textCapitalization: widget.props.textCapitalization, - autofocus: widget.props.autofocus, - autocorrect: widget.props.autoCorrect, - isFloating: effectiveComposerLocation == .floating, - ), - ), - ), - SizeTransition( - sizeFactor: _pickerAnimation, - // ignore: deprecated_member_use - axisAlignment: -1, - child: _buildInlineAttachmentPicker(context), + textInputAction: widget.props.textInputAction, + keyboardType: widget.props.keyboardType, + textCapitalization: widget.props.textCapitalization, + autofocus: widget.props.autofocus, + autocorrect: widget.props.autoCorrect, + isFloating: isFloating, ), + ), + ); + + return PopScope( + canPop: !_isPickerVisible, + onPopInvokedWithResult: (didPop, _) { + if (!didPop) _hidePicker(); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + // Reversed paint order so the pill (and its shadow) paints on top + // of the picker panel. VerticalDirection.up keeps the pill visually + // above the picker while making it the last-painted child. + verticalDirection: VerticalDirection.up, + children: [ + SizeTransition( + sizeFactor: _pickerAnimation, + // ignore: deprecated_member_use + axisAlignment: -1, + child: _buildInlineAttachmentPicker(context), + ), + // Gradient wraps only the pill — not the picker sibling — so + // the gradient height tracks the pill and doesn't stretch when + // the picker opens or closes. + if (isFloating) _buildFloatingComposerBand(context, pill) else pill, + ], + ), + ); + }, + ); + } + + /// Wraps [child] (the pill widget) with a floating gradient background that + /// fades from transparent at the top to solid + /// [StreamColorScheme.backgroundElevation1] at the bottom. + /// + /// Applied to just the pill [DropTarget] so the gradient height tracks the + /// pill (growing with attachments) and is unaffected by the picker opening. + Widget _buildFloatingComposerBand(BuildContext context, Widget child) { + final bandColor = context.streamColorScheme.backgroundElevation1; + + return DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + bandColor.withAlpha(0x00), // transparent at top of composer band + bandColor.withAlpha(0x40), // ~25 % + bandColor.withAlpha(0xA8), // ~66 % + bandColor.withAlpha(0xE8), // ~91 % + bandColor, // solid at bottom (safe area) ], + stops: const [0.0, 0.55, 0.75, 0.90, 1.0], ), ), + child: child, ); } From 4b0a70013250612d7f54e673df4cbccac3458ab2 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 10:28:17 +0200 Subject: [PATCH 19/48] fix attachmentpicker background --- .../stream_message_composer.dart | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 180a7c1cc6..8f7e1eeaf7 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -392,6 +392,7 @@ class MessageComposerProps { /// The location of the message composer. final ComposerLocation? composerLocation; + /// Returns a copy of this [MessageComposerProps] with the given fields /// replaced with new values. MessageComposerProps copyWith({ @@ -1031,28 +1032,28 @@ class DefaultStreamMessageComposerState extends State Date: Fri, 12 Jun 2026 10:40:02 +0200 Subject: [PATCH 20/48] Add sample app config --- docs/docs_screenshots/pubspec.yaml | 2 +- melos.yaml | 2 +- packages/stream_chat_flutter/pubspec.yaml | 2 +- sample_app/lib/app.dart | 34 ++++++++++--------- sample_app/lib/config/sample_app_config.dart | 25 ++++++++++++++ .../lib/config/sample_app_config_screen.dart | 13 +++++++ 6 files changed, 59 insertions(+), 19 deletions(-) diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index df13ee5a7f..e4850a697d 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: 43f90a46d8092ca2c1e24c36af06f05fc0c633a6 + ref: bb75889af53a2a9e42939adb4e222680b18142d4 path: packages/stream_core_flutter dev_dependencies: diff --git a/melos.yaml b/melos.yaml index fa5882a546..960b81fb44 100644 --- a/melos.yaml +++ b/melos.yaml @@ -101,7 +101,7 @@ command: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: 43f90a46d8092ca2c1e24c36af06f05fc0c633a6 + ref: bb75889af53a2a9e42939adb4e222680b18142d4 path: packages/stream_core_flutter synchronized: ^3.4.0 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 791b2e8fa1..374ccefdd0 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: # ignore: invalid_dependency git: url: https://github.com/GetStream/stream-core-flutter - ref: 43f90a46d8092ca2c1e24c36af06f05fc0c633a6 + ref: bb75889af53a2a9e42939adb4e222680b18142d4 path: packages/stream_core_flutter svg_icon_widget: ^0.0.1 synchronized: ^3.4.0 diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index de507c91e6..6e69708419 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -170,23 +170,25 @@ class _StreamChatSampleAppState extends State child: Builder( builder: (context) { final config = context.sampleAppConfig; + + StreamAppStyle appStyle() => switch (config.appStyle) { + SampleAppStyle.regular => const StreamAppStyle.regular(), + SampleAppStyle.floating => const StreamAppStyle.floating(), + }; + + ThemeData theme(Brightness brightness) => ThemeData( + brightness: brightness, + extensions: [ + StreamTheme( + brightness: brightness, + appStyle: appStyle(), + ), + ], + ); + return MaterialApp.router( - theme: ThemeData( - brightness: .light, - extensions: [ - StreamTheme.light().copyWith( - appStyle: const StreamAppStyle.floating(), - ), - ], - ), - darkTheme: ThemeData( - brightness: .dark, - extensions: [ - StreamTheme.dark().copyWith( - appStyle: const StreamAppStyle.floating(), - ), - ], - ), + theme: theme(.light), + darkTheme: theme(.dark), themeMode: config.themeMode, locale: config.locale, supportedLocales: supportedLocales, diff --git a/sample_app/lib/config/sample_app_config.dart b/sample_app/lib/config/sample_app_config.dart index 40bdac4021..13d5fbf0c2 100644 --- a/sample_app/lib/config/sample_app_config.dart +++ b/sample_app/lib/config/sample_app_config.dart @@ -3,11 +3,25 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; +// --------------------------------------------------------------------------- +// AppStyle enum +// --------------------------------------------------------------------------- + +/// The visual style for the app's UI chrome (app bar, composer, bottom bar). +enum SampleAppStyle { + /// Standard docked composer with solid app bar and bottom bar. + regular, + + /// Floating composer with translucent overlapping chrome. + floating, +} + // --------------------------------------------------------------------------- // Preference keys // --------------------------------------------------------------------------- const _kThemeMode = 'config.themeMode'; +const _kAppStyle = 'config.appStyle'; const _kForceRtl = 'config.forceRtl'; const _kEnableReminderActions = 'config.enableReminderActions'; const _kEnableDeleteForMe = 'config.enableDeleteForMe'; @@ -38,6 +52,7 @@ class SampleAppConfigData { factory SampleAppConfigData({ Locale? locale, ThemeMode themeMode = .system, + SampleAppStyle appStyle = .floating, bool forceRtl = false, bool enableReminderActions = false, bool enableDeleteForMe = false, @@ -50,6 +65,7 @@ class SampleAppConfigData { }) { return SampleAppConfigData.raw( themeMode: themeMode, + appStyle: appStyle, locale: locale, forceRtl: forceRtl, enableReminderActions: enableReminderActions, @@ -66,6 +82,7 @@ class SampleAppConfigData { /// Raw constructor used internally and by persistence. const SampleAppConfigData.raw({ required this.themeMode, + required this.appStyle, required this.locale, required this.forceRtl, required this.enableReminderActions, @@ -81,8 +98,10 @@ class SampleAppConfigData { /// Loads config from [StreamingSharedPreferences], falling back to defaults. factory SampleAppConfigData.fromPreferences(StreamingSharedPreferences prefs) { final localeStr = prefs.getString(_kLocale, defaultValue: '').getValue(); + final appStyleIndex = prefs.getInt(_kAppStyle, defaultValue: SampleAppStyle.floating.index).getValue(); return SampleAppConfigData.raw( themeMode: ThemeMode.values[prefs.getInt(_kThemeMode, defaultValue: ThemeMode.system.index).getValue()], + appStyle: SampleAppStyle.values[appStyleIndex.clamp(0, SampleAppStyle.values.length - 1)], locale: localeStr.isEmpty ? null : Locale(localeStr), forceRtl: prefs.getBool(_kForceRtl, defaultValue: false).getValue(), enableReminderActions: prefs.getBool(_kEnableReminderActions, defaultValue: false).getValue(), @@ -101,6 +120,9 @@ class SampleAppConfigData { /// The theme mode for the app (system, light, dark). final ThemeMode themeMode; + /// The visual style for the app chrome (app bar, composer, bottom bar). + final SampleAppStyle appStyle; + /// The locale override for the app. When null, the system locale is used. final Locale? locale; @@ -141,6 +163,7 @@ class SampleAppConfigData { /// pass explicitly as `null` to reset to default/system. SampleAppConfigData copyWith({ ThemeMode? themeMode, + SampleAppStyle? appStyle, Object? locale = _sentinel, bool? forceRtl, bool? enableReminderActions, @@ -154,6 +177,7 @@ class SampleAppConfigData { }) { return SampleAppConfigData.raw( themeMode: themeMode ?? this.themeMode, + appStyle: appStyle ?? this.appStyle, locale: locale == _sentinel ? this.locale : locale as Locale?, forceRtl: forceRtl ?? this.forceRtl, enableReminderActions: enableReminderActions ?? this.enableReminderActions, @@ -174,6 +198,7 @@ class SampleAppConfigData { /// Persists all fields to [StreamingSharedPreferences]. void saveToPreferences(StreamingSharedPreferences prefs) { prefs.setInt(_kThemeMode, themeMode.index); + prefs.setInt(_kAppStyle, appStyle.index); prefs.setString(_kLocale, locale?.languageCode ?? ''); prefs.setBool(_kForceRtl, forceRtl); prefs.setBool(_kEnableReminderActions, enableReminderActions); diff --git a/sample_app/lib/config/sample_app_config_screen.dart b/sample_app/lib/config/sample_app_config_screen.dart index f6bfffa0e8..02ae33e58e 100644 --- a/sample_app/lib/config/sample_app_config_screen.dart +++ b/sample_app/lib/config/sample_app_config_screen.dart @@ -43,6 +43,19 @@ class SampleAppConfigScreen extends StatelessWidget { }, onChanged: (v) => SampleAppConfig.update(context, config.copyWith(themeMode: v)), ), + _SegmentedRow( + title: 'App Style', + value: config.appStyle, + segments: const { + SampleAppStyle.regular: 'Regular', + SampleAppStyle.floating: 'Floating', + }, + segmentIcons: const { + SampleAppStyle.regular: Icons.web_asset_outlined, + SampleAppStyle.floating: Icons.filter_none_outlined, + }, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(appStyle: v)), + ), _LocaleRow(config: config), _SwitchRow( icon: icons.reorder, From 9ba1ba8dcb6082e6b428cdbd17de1bce0d3aafe2 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 13:44:53 +0200 Subject: [PATCH 21/48] Fix extra padding on loading skeletons --- .../stream_channel_list_skeleton_loading.dart | 1 + .../thread_scroll_view/stream_thread_list_skeleton_loading.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_skeleton_loading.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_skeleton_loading.dart index 005f4d3fe3..c76f8f1101 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_skeleton_loading.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_skeleton_loading.dart @@ -19,6 +19,7 @@ class StreamChannelListSkeletonLoading extends StatelessWidget { Widget build(BuildContext context) { return StreamSkeletonLoading( child: ListView.separated( + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: itemCount, separatorBuilder: (context, index) => const SizedBox(height: 1), diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_skeleton_loading.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_skeleton_loading.dart index 6a92bc91ee..fb6300e8ee 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_skeleton_loading.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_skeleton_loading.dart @@ -19,6 +19,7 @@ class StreamThreadListSkeletonLoading extends StatelessWidget { Widget build(BuildContext context) { return StreamSkeletonLoading( child: ListView.separated( + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: itemCount, separatorBuilder: (context, index) => const SizedBox(height: 1), From dfedb968e97bb12e625928a40ba4dc2210b254b9 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 14:34:40 +0200 Subject: [PATCH 22/48] Update all sample app pages --- .../lib/config/sample_app_config_screen.dart | 279 +++++++++--------- .../lib/pages/advanced_options_page.dart | 5 +- .../pages/channel_file_display_screen.dart | 80 ++--- .../pages/channel_media_display_screen.dart | 65 ++-- sample_app/lib/pages/chat_info_screen.dart | 47 +-- .../lib/pages/group_chat_details_screen.dart | 210 ++++++------- sample_app/lib/pages/group_info_screen.dart | 51 ++-- sample_app/lib/pages/new_chat_screen.dart | 4 +- .../lib/pages/new_group_chat_screen.dart | 4 +- .../lib/pages/pinned_messages_screen.dart | 16 +- 10 files changed, 401 insertions(+), 360 deletions(-) diff --git a/sample_app/lib/config/sample_app_config_screen.dart b/sample_app/lib/config/sample_app_config_screen.dart index 02ae33e58e..faa08bc543 100644 --- a/sample_app/lib/config/sample_app_config_screen.dart +++ b/sample_app/lib/config/sample_app_config_screen.dart @@ -13,155 +13,160 @@ class SampleAppConfigScreen extends StatelessWidget { final spacing = context.streamSpacing; final icons = context.streamIcons; - return Scaffold( + return StreamScaffold( backgroundColor: colorScheme.backgroundApp, appBar: StreamAppBar(title: const Text('Configuration')), - body: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: spacing.md), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: spacing.xs), - - // ── Appearance ── - const _SectionHeader(title: 'Appearance'), - SizedBox(height: spacing.xs), - _SettingsCard( + body: Builder( + builder: (context) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; + return SingleChildScrollView( + padding: EdgeInsets.fromLTRB(spacing.md, topInset, spacing.md, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _SegmentedRow( - title: 'Theme', - value: config.themeMode, - segments: const { - ThemeMode.system: 'System', - ThemeMode.light: 'Light', - ThemeMode.dark: 'Dark', - }, - segmentIcons: const { - ThemeMode.system: Icons.brightness_auto_outlined, - ThemeMode.light: Icons.light_mode_outlined, - ThemeMode.dark: Icons.dark_mode_outlined, - }, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(themeMode: v)), - ), - _SegmentedRow( - title: 'App Style', - value: config.appStyle, - segments: const { - SampleAppStyle.regular: 'Regular', - SampleAppStyle.floating: 'Floating', - }, - segmentIcons: const { - SampleAppStyle.regular: Icons.web_asset_outlined, - SampleAppStyle.floating: Icons.filter_none_outlined, - }, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(appStyle: v)), - ), - _LocaleRow(config: config), - _SwitchRow( - icon: icons.reorder, - title: 'Force RTL', - subtitle: 'Right-to-left layout direction', - value: config.forceRtl, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(forceRtl: v)), + SizedBox(height: spacing.xs), + + // ── Appearance ── + const _SectionHeader(title: 'Appearance'), + SizedBox(height: spacing.xs), + _SettingsCard( + children: [ + _SegmentedRow( + title: 'Theme', + value: config.themeMode, + segments: const { + ThemeMode.system: 'System', + ThemeMode.light: 'Light', + ThemeMode.dark: 'Dark', + }, + segmentIcons: const { + ThemeMode.system: Icons.brightness_auto_outlined, + ThemeMode.light: Icons.light_mode_outlined, + ThemeMode.dark: Icons.dark_mode_outlined, + }, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(themeMode: v)), + ), + _SegmentedRow( + title: 'App Style', + value: config.appStyle, + segments: const { + SampleAppStyle.regular: 'Regular', + SampleAppStyle.floating: 'Floating', + }, + segmentIcons: const { + SampleAppStyle.regular: Icons.web_asset_outlined, + SampleAppStyle.floating: Icons.filter_none_outlined, + }, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(appStyle: v)), + ), + _LocaleRow(config: config), + _SwitchRow( + icon: icons.reorder, + title: 'Force RTL', + subtitle: 'Right-to-left layout direction', + value: config.forceRtl, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(forceRtl: v)), + ), + ], ), - ], - ), - - SizedBox(height: spacing.xl), - // ── Features ── - const _SectionHeader(title: 'Features'), - SizedBox(height: spacing.xs), - _SettingsCard( - children: [ - _SwitchRow( - icon: icons.bell, - title: 'Reminders', - subtitle: 'Remind me, Save for later, Edit', - value: config.enableReminderActions, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enableReminderActions: v)), - ), - _SwitchRow( - icon: icons.delete, - title: 'Delete for Me', - subtitle: 'Delete message for current user', - value: config.enableDeleteForMe, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enableDeleteForMe: v)), - ), - _SwitchRow( - icon: icons.info, - title: 'Message Info', - subtitle: 'Show delivery info sheet', - value: config.enableMessageInfo, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enableMessageInfo: v)), - ), - _SwitchRow( - icon: icons.location, - title: 'Location Sharing', - subtitle: 'Attachment builder and picker', - value: config.enableLocationSharing, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enableLocationSharing: v)), + SizedBox(height: spacing.xl), + + // ── Features ── + const _SectionHeader(title: 'Features'), + SizedBox(height: spacing.xs), + _SettingsCard( + children: [ + _SwitchRow( + icon: icons.bell, + title: 'Reminders', + subtitle: 'Remind me, Save for later, Edit', + value: config.enableReminderActions, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enableReminderActions: v)), + ), + _SwitchRow( + icon: icons.delete, + title: 'Delete for Me', + subtitle: 'Delete message for current user', + value: config.enableDeleteForMe, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enableDeleteForMe: v)), + ), + _SwitchRow( + icon: icons.info, + title: 'Message Info', + subtitle: 'Show delivery info sheet', + value: config.enableMessageInfo, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enableMessageInfo: v)), + ), + _SwitchRow( + icon: icons.location, + title: 'Location Sharing', + subtitle: 'Attachment builder and picker', + value: config.enableLocationSharing, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enableLocationSharing: v)), + ), + ], ), - ], - ), - SizedBox(height: spacing.xl), - - // ── Chat ── - const _SectionHeader(title: 'Chat'), - SizedBox(height: spacing.xs), - _SettingsCard( - children: [ - _SwitchRow( - icon: icons.edit, - title: 'Draft Messages', - subtitle: 'Enable draft message saving', - value: config.draftMessagesEnabled, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(draftMessagesEnabled: v)), - ), - _SwitchRow( - icon: icons.emoji, - title: 'Unique Reactions', - subtitle: 'New reaction replaces existing', - value: config.enforceUniqueReactions, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enforceUniqueReactions: v)), + SizedBox(height: spacing.xl), + + // ── Chat ── + const _SectionHeader(title: 'Chat'), + SizedBox(height: spacing.xs), + _SettingsCard( + children: [ + _SwitchRow( + icon: icons.edit, + title: 'Draft Messages', + subtitle: 'Enable draft message saving', + value: config.draftMessagesEnabled, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(draftMessagesEnabled: v)), + ), + _SwitchRow( + icon: icons.emoji, + title: 'Unique Reactions', + subtitle: 'New reaction replaces existing', + value: config.enforceUniqueReactions, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(enforceUniqueReactions: v)), + ), + ], ), - ], - ), - SizedBox(height: spacing.xl), - - // ── Reactions ── - const _SectionHeader(title: 'Reactions'), - SizedBox(height: spacing.xs), - _SettingsCard( - children: [ - _SegmentedRow( - title: 'Reaction Type', - value: config.reactionType, - segments: const { - null: 'Default', - StreamReactionsType.segmented: 'Segmented', - StreamReactionsType.clustered: 'Clustered', - }, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(reactionType: v)), - ), - _SegmentedRow( - title: 'Reaction Position', - value: config.reactionPosition, - segments: const { - null: 'Default', - StreamReactionsPosition.header: 'Header', - StreamReactionsPosition.footer: 'Footer', - }, - onChanged: (v) => SampleAppConfig.update(context, config.copyWith(reactionPosition: v)), + SizedBox(height: spacing.xl), + + // ── Reactions ── + const _SectionHeader(title: 'Reactions'), + SizedBox(height: spacing.xs), + _SettingsCard( + children: [ + _SegmentedRow( + title: 'Reaction Type', + value: config.reactionType, + segments: const { + null: 'Default', + StreamReactionsType.segmented: 'Segmented', + StreamReactionsType.clustered: 'Clustered', + }, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(reactionType: v)), + ), + _SegmentedRow( + title: 'Reaction Position', + value: config.reactionPosition, + segments: const { + null: 'Default', + StreamReactionsPosition.header: 'Header', + StreamReactionsPosition.footer: 'Footer', + }, + onChanged: (v) => SampleAppConfig.update(context, config.copyWith(reactionPosition: v)), + ), + ], ), + + SizedBox(height: spacing.xxl), ], ), - - SizedBox(height: spacing.xxl), - ], - ), + ); + }, ), ); } diff --git a/sample_app/lib/pages/advanced_options_page.dart b/sample_app/lib/pages/advanced_options_page.dart index 525e4cf2da..6533a122f9 100644 --- a/sample_app/lib/pages/advanced_options_page.dart +++ b/sample_app/lib/pages/advanced_options_page.dart @@ -100,13 +100,14 @@ class _AdvancedOptionsPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return StreamScaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: StreamAppBar(title: const Text('Custom settings')), body: Builder( builder: (context) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + padding: EdgeInsets.fromLTRB(16, 16 + topInset, 16, 0), child: Form( key: _formKey, child: Column( diff --git a/sample_app/lib/pages/channel_file_display_screen.dart b/sample_app/lib/pages/channel_file_display_screen.dart index 5cc931d83a..b0effdb1b5 100644 --- a/sample_app/lib/pages/channel_file_display_screen.dart +++ b/sample_app/lib/pages/channel_file_display_screen.dart @@ -39,50 +39,54 @@ class _ChannelFileDisplayScreenState extends State { @override Widget build(BuildContext context) { final colorScheme = context.streamColorScheme; - return Scaffold( + return StreamScaffold( backgroundColor: colorScheme.backgroundApp, appBar: StreamAppBar(title: const Text('Files')), body: ValueListenableBuilder>( valueListenable: _controller, - builder: (context, value, _) => value.when( - (items, nextPageKey, _) { - // Flatten messages → individual file attachments paired with - // the message timestamp we'll bucket on. - final entries = <_FileEntry>[ - for (final response in items) - for (final attachment in response.message.attachments) - if (attachment.type == 'file') - _FileEntry( - attachment: attachment, - sentAt: response.message.createdAt, - ), - ]; - - if (entries.isEmpty) return const Center(child: _EmptyState()); - - // Pre-build a flat row list — interleave a header row above - // each month bucket so a single ListView.builder can render - // both kinds of rows without a CustomScrollView + slivers. - final rows = _buildRows(entries); - - return LazyLoadScrollView( - onEndOfPage: () async { - if (nextPageKey != null) await _controller.loadMore(nextPageKey); - }, - child: ListView.builder( - itemCount: rows.length, - itemBuilder: (context, index) => rows[index].build(context), + builder: (context, value, _) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; + return value.when( + (items, nextPageKey, _) { + // Flatten messages → individual file attachments paired with + // the message timestamp we'll bucket on. + final entries = <_FileEntry>[ + for (final response in items) + for (final attachment in response.message.attachments) + if (attachment.type == 'file') + _FileEntry( + attachment: attachment, + sentAt: response.message.createdAt, + ), + ]; + + if (entries.isEmpty) return const Center(child: _EmptyState()); + + // Pre-build a flat row list — interleave a header row above + // each month bucket so a single ListView.builder can render + // both kinds of rows without a CustomScrollView + slivers. + final rows = _buildRows(entries); + + return LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) await _controller.loadMore(nextPageKey); + }, + child: ListView.builder( + padding: EdgeInsets.only(top: topInset), + itemCount: rows.length, + itemBuilder: (context, index) => rows[index].build(context), + ), + ); + }, + loading: () => const Center(child: StreamScrollViewLoadingWidget()), + error: (_) => Center( + child: StreamScrollViewErrorWidget( + errorTitle: const Text('Failed to load files'), + onRetryPressed: _controller.refresh, ), - ); - }, - loading: () => const Center(child: StreamScrollViewLoadingWidget()), - error: (_) => Center( - child: StreamScrollViewErrorWidget( - errorTitle: const Text('Failed to load files'), - onRetryPressed: _controller.refresh, ), - ), - ), + ); + }, ), ); } diff --git a/sample_app/lib/pages/channel_media_display_screen.dart b/sample_app/lib/pages/channel_media_display_screen.dart index 3aac2fabf4..01351a702a 100644 --- a/sample_app/lib/pages/channel_media_display_screen.dart +++ b/sample_app/lib/pages/channel_media_display_screen.dart @@ -38,44 +38,49 @@ class _ChannelMediaDisplayScreenState extends State { @override Widget build(BuildContext context) { final colorScheme = context.streamColorScheme; - return Scaffold( + return StreamScaffold( backgroundColor: colorScheme.backgroundApp, appBar: StreamAppBar(title: Text(context.translations.photosAndVideosLabel)), body: ValueListenableBuilder>( valueListenable: _controller, - builder: (context, value, _) => value.when( - (items, nextPageKey, _) { - // Flatten messages → individual image/video attachments. - // Excludes link previews (`ogScrapeUrl != null`) so we don't - // render every shared URL's thumbnail in the grid. - final attachments = [ - for (final response in items) - ...response.message.toMediaGalleryAttachments( - filter: (a) => - (a.type == AttachmentType.image || a.type == AttachmentType.video) && a.ogScrapeUrl == null, - ), - ]; + builder: (context, value, _) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; + final spacing = context.streamSpacing; + return value.when( + (items, nextPageKey, _) { + // Flatten messages → individual image/video attachments. + // Excludes link previews (`ogScrapeUrl != null`) so we don't + // render every shared URL's thumbnail in the grid. + final attachments = [ + for (final response in items) + ...response.message.toMediaGalleryAttachments( + filter: (a) => + (a.type == AttachmentType.image || a.type == AttachmentType.video) && a.ogScrapeUrl == null, + ), + ]; - if (attachments.isEmpty) return const Center(child: _EmptyState()); + if (attachments.isEmpty) return const Center(child: _EmptyState()); - return LazyLoadScrollView( - onEndOfPage: () async { - if (nextPageKey != null) await _controller.loadMore(nextPageKey); - }, - child: StreamMediaGallery( - attachments: attachments, - onItemTap: (index) => _openPreview(context, attachments, index), + return LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) await _controller.loadMore(nextPageKey); + }, + child: StreamMediaGallery( + attachments: attachments, + padding: EdgeInsets.fromLTRB(spacing.xxxs, topInset + spacing.xxxs, spacing.xxxs, spacing.xxxs), + onItemTap: (index) => _openPreview(context, attachments, index), + ), + ); + }, + loading: () => const Center(child: StreamScrollViewLoadingWidget()), + error: (_) => Center( + child: StreamScrollViewErrorWidget( + errorTitle: const Text('Failed to load media'), + onRetryPressed: _controller.refresh, ), - ); - }, - loading: () => const Center(child: StreamScrollViewLoadingWidget()), - error: (_) => Center( - child: StreamScrollViewErrorWidget( - errorTitle: const Text('Failed to load media'), - onRetryPressed: _controller.refresh, ), - ), - ), + ); + }, ), ); } diff --git a/sample_app/lib/pages/chat_info_screen.dart b/sample_app/lib/pages/chat_info_screen.dart index 77906922ed..e9d605132a 100644 --- a/sample_app/lib/pages/chat_info_screen.dart +++ b/sample_app/lib/pages/chat_info_screen.dart @@ -27,31 +27,36 @@ class ChatInfoScreen extends StatelessWidget { final spacing = context.streamSpacing; final colorScheme = context.streamColorScheme; - return Scaffold( + return StreamScaffold( backgroundColor: colorScheme.backgroundApp, appBar: StreamAppBar(title: const Text('Contact Info')), // Action / chevron icons share a uniform 20px size — set once at the // top of the body so individual rows stay style-free. - body: IconTheme.merge( - data: const IconThemeData(size: 20), - child: SingleChildScrollView( - padding: .directional( - top: spacing.xxl, - bottom: spacing.xxxl, - start: spacing.md, - end: spacing.md, - ), - child: Column( - mainAxisSize: .min, - children: [ - _ContactInfoHeader(user: user), - SizedBox(height: spacing.xxl), - const _MediaSection(), - SizedBox(height: spacing.md), - const _ActionsSection(), - ], - ), - ), + body: Builder( + builder: (context) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; + return IconTheme.merge( + data: const IconThemeData(size: 20), + child: SingleChildScrollView( + padding: .directional( + top: spacing.xxl + topInset, + bottom: spacing.xxxl, + start: spacing.md, + end: spacing.md, + ), + child: Column( + mainAxisSize: .min, + children: [ + _ContactInfoHeader(user: user), + SizedBox(height: spacing.xxl), + const _MediaSection(), + SizedBox(height: spacing.md), + const _ActionsSection(), + ], + ), + ), + ); + }, ), ); } diff --git a/sample_app/lib/pages/group_chat_details_screen.dart b/sample_app/lib/pages/group_chat_details_screen.dart index 08fc6dbb62..9a7b1adc80 100644 --- a/sample_app/lib/pages/group_chat_details_screen.dart +++ b/sample_app/lib/pages/group_chat_details_screen.dart @@ -48,7 +48,7 @@ class _GroupChatDetailsScreenState extends State { GoRouter.of(context).pop(); return false; }, - child: Scaffold( + child: StreamScaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: StreamAppBar( title: const Text('Name of Group Chat'), @@ -105,117 +105,123 @@ class _GroupChatDetailsScreenState extends State { tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, message: statusString, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), - child: Row( - children: [ - Text( - 'Name'.toUpperCase(), - style: TextStyle( - fontSize: 12, - color: context.streamColorScheme.textSecondary, - ), - ), - const SizedBox(width: 16), - Expanded( - child: TextField( - controller: _groupNameController, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - hintText: 'Choose a group chat name', - hintStyle: TextStyle( - fontSize: 14, + child: Builder( + builder: (context) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; + return Column( + children: [ + if (topInset > 0) SizedBox(height: topInset), + Padding( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), + child: Row( + children: [ + Text( + 'Name'.toUpperCase(), + style: TextStyle( + fontSize: 12, color: context.streamColorScheme.textSecondary, ), ), - ), + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: _groupNameController, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + hintText: 'Choose a group chat name', + hintStyle: TextStyle( + fontSize: 14, + color: context.streamColorScheme.textSecondary, + ), + ), + ), + ), + ], ), - ], - ), - ), - Container( - width: double.maxFinite, - decoration: BoxDecoration( - color: context.streamColorScheme.backgroundElevation1, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, ), - child: Text( - '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', - style: TextStyle( - color: context.streamColorScheme.textSecondary, + Container( + width: double.maxFinite, + decoration: BoxDecoration( + color: context.streamColorScheme.backgroundElevation1, ), - ), - ), - ), - AnimatedBuilder( - animation: widget.groupChatState, - builder: (context, child) { - return Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: ListView.separated( - itemCount: widget.groupChatState.users.length + 1, - separatorBuilder: (_, __) => Container( - height: 1, - color: context.streamColorScheme.borderDefault, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', + style: TextStyle( + color: context.streamColorScheme.textSecondary, ), - itemBuilder: (_, index) { - if (index == widget.groupChatState.users.length) { - return Container( + ), + ), + ), + AnimatedBuilder( + animation: widget.groupChatState, + builder: (context, child) { + return Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: ListView.separated( + itemCount: widget.groupChatState.users.length + 1, + separatorBuilder: (_, __) => Container( height: 1, color: context.streamColorScheme.borderDefault, - ); - } - final user = widget.groupChatState.users.elementAt(index); - return ListTile( - key: ObjectKey(user), - leading: StreamUserAvatar( - size: .lg, - user: user, - ), - title: Text( - user.name, - style: const TextStyle(fontWeight: FontWeight.bold), ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - trailing: IconButton( - icon: Icon( - Icons.clear_rounded, - color: context.streamColorScheme.textPrimary, - ), - padding: EdgeInsets.zero, - splashRadius: 24, - onPressed: () { - widget.groupChatState.removeUser(user); - if (widget.groupChatState.users.isEmpty) { - GoRouter.of(context).pop(); - } - }, - ), - ); - }, - ), - ), - ); - }, - ), - ], + itemBuilder: (_, index) { + if (index == widget.groupChatState.users.length) { + return Container( + height: 1, + color: context.streamColorScheme.borderDefault, + ); + } + final user = widget.groupChatState.users.elementAt(index); + return ListTile( + key: ObjectKey(user), + leading: StreamUserAvatar( + size: .lg, + user: user, + ), + title: Text( + user.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + trailing: IconButton( + icon: Icon( + Icons.clear_rounded, + color: context.streamColorScheme.textPrimary, + ), + padding: EdgeInsets.zero, + splashRadius: 24, + onPressed: () { + widget.groupChatState.removeUser(user); + if (widget.groupChatState.users.isEmpty) { + GoRouter.of(context).pop(); + } + }, + ), + ); + }, + ), + ), + ); + }, + ), + ], + ); + }, ), ); }, diff --git a/sample_app/lib/pages/group_info_screen.dart b/sample_app/lib/pages/group_info_screen.dart index 3f26366c6f..0d1ed66cea 100644 --- a/sample_app/lib/pages/group_info_screen.dart +++ b/sample_app/lib/pages/group_info_screen.dart @@ -24,7 +24,7 @@ class GroupInfoScreen extends StatelessWidget { final colorScheme = context.streamColorScheme; final channel = StreamChannel.of(context).channel; - return Scaffold( + return StreamScaffold( backgroundColor: colorScheme.backgroundApp, appBar: StreamAppBar( title: const Text('Group Info'), @@ -41,28 +41,33 @@ class GroupInfoScreen extends StatelessWidget { ), // Action / chevron icons share a uniform 20px size — set once at the // top of the body so individual rows stay style-free. - body: IconTheme.merge( - data: const IconThemeData(size: 20), - child: SingleChildScrollView( - padding: .directional( - top: spacing.xxl, - bottom: spacing.xxxl, - start: spacing.md, - end: spacing.md, - ), - child: Column( - mainAxisSize: .min, - children: [ - const _GroupInfoHeader(), - SizedBox(height: spacing.xxl), - const _MediaSection(), - SizedBox(height: spacing.md), - const _MembersSection(), - SizedBox(height: spacing.md), - const _ActionsSection(), - ], - ), - ), + body: Builder( + builder: (context) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; + return IconTheme.merge( + data: const IconThemeData(size: 20), + child: SingleChildScrollView( + padding: .directional( + top: spacing.xxl + topInset, + bottom: spacing.xxxl, + start: spacing.md, + end: spacing.md, + ), + child: Column( + mainAxisSize: .min, + children: [ + const _GroupInfoHeader(), + SizedBox(height: spacing.xxl), + const _MediaSection(), + SizedBox(height: spacing.md), + const _MembersSection(), + SizedBox(height: spacing.md), + const _ActionsSection(), + ], + ), + ), + ); + }, ), ); } diff --git a/sample_app/lib/pages/new_chat_screen.dart b/sample_app/lib/pages/new_chat_screen.dart index 673292ef39..fecf2a56a4 100644 --- a/sample_app/lib/pages/new_chat_screen.dart +++ b/sample_app/lib/pages/new_chat_screen.dart @@ -138,11 +138,12 @@ class _NewChatScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return StreamScaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: StreamAppBar(title: const Text('New Chat')), body: StreamConnectionStatusBuilder( statusBuilder: (context, status) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; var statusString = ''; var showStatus = true; @@ -169,6 +170,7 @@ class _NewChatScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (topInset > 0) SizedBox(height: topInset), ChipsInputTextField( key: _chipInputTextFieldStateKey, controller: _controller, diff --git a/sample_app/lib/pages/new_group_chat_screen.dart b/sample_app/lib/pages/new_group_chat_screen.dart index b5d9c2eb53..5f9565fbdf 100644 --- a/sample_app/lib/pages/new_group_chat_screen.dart +++ b/sample_app/lib/pages/new_group_chat_screen.dart @@ -66,7 +66,7 @@ class _NewGroupChatScreenState extends State { animation: groupChatState, builder: (context, child) { final state = groupChatState; - return Scaffold( + return StreamScaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: StreamAppBar( title: const Text('Add Group Members'), @@ -108,7 +108,9 @@ class _NewGroupChatScreenState extends State { child: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; return [ + if (topInset > 0) SliverToBoxAdapter(child: SizedBox(height: topInset)), SliverToBoxAdapter( child: SearchTextField( controller: _controller, diff --git a/sample_app/lib/pages/pinned_messages_screen.dart b/sample_app/lib/pages/pinned_messages_screen.dart index f2b52a4b16..e07c2e1aa3 100644 --- a/sample_app/lib/pages/pinned_messages_screen.dart +++ b/sample_app/lib/pages/pinned_messages_screen.dart @@ -37,13 +37,19 @@ class _PinnedMessagesScreenState extends State { Widget build(BuildContext context) { final colorScheme = context.streamColorScheme; - return Scaffold( + return StreamScaffold( backgroundColor: colorScheme.backgroundApp, appBar: StreamAppBar(title: const Text('Pinned Messages')), - body: StreamMessageSearchListView( - controller: _controller, - emptyBuilder: (_) => const Center(child: _EmptyState()), - onMessageTap: _openMessage, + body: Builder( + builder: (context) { + final topInset = StreamScaffoldInsets.maybeOf(context)?.topPadding ?? 0.0; + return StreamMessageSearchListView( + controller: _controller, + padding: EdgeInsets.only(top: topInset), + emptyBuilder: (_) => const Center(child: _EmptyState()), + onMessageTap: _openMessage, + ); + }, ), ); } From c431a3b0ad47abb596f215991eefd0893b77bf12 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 14:35:26 +0200 Subject: [PATCH 23/48] update core dependency --- docs/docs_screenshots/pubspec.yaml | 2 +- melos.yaml | 2 +- packages/stream_chat_flutter/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index e4850a697d..d1b301dc8c 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: bb75889af53a2a9e42939adb4e222680b18142d4 + ref: 6f8ba0c07a6ffbc6e0ea96c560f010cf48b8012b path: packages/stream_core_flutter dev_dependencies: diff --git a/melos.yaml b/melos.yaml index 960b81fb44..1b0b621311 100644 --- a/melos.yaml +++ b/melos.yaml @@ -101,7 +101,7 @@ command: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: bb75889af53a2a9e42939adb4e222680b18142d4 + ref: 6f8ba0c07a6ffbc6e0ea96c560f010cf48b8012b path: packages/stream_core_flutter synchronized: ^3.4.0 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 374ccefdd0..eab51be6bc 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: # ignore: invalid_dependency git: url: https://github.com/GetStream/stream-core-flutter - ref: bb75889af53a2a9e42939adb4e222680b18142d4 + ref: 6f8ba0c07a6ffbc6e0ea96c560f010cf48b8012b path: packages/stream_core_flutter svg_icon_widget: ^0.0.1 synchronized: ^3.4.0 From fb4f234f30eab3900f40242d1d3c887429781a54 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 14:55:37 +0200 Subject: [PATCH 24/48] Add global message list config --- packages/stream_chat_flutter/CHANGELOG.md | 1 + .../lib/src/channel/channel_page.dart | 11 ---- .../lib/src/channel/thread_page.dart | 5 -- .../message_list_view/message_list_view.dart | 64 +++++++++++-------- .../lib/src/stream_chat_configuration.dart | 12 ++++ sample_app/lib/app.dart | 4 ++ sample_app/lib/routes/app_routes.dart | 1 - 7 files changed, 54 insertions(+), 44 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 3e97b702a5..f8e94349f5 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -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 diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index 48e21105f4..e3d18c4212 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -8,7 +8,6 @@ class StreamChannelPage extends StatefulWidget { super.key, this.initialScrollIndex, this.initialAlignment, - this.highlightInitialMessage = false, this.onBackPressed, this.onChannelAvatarPressed, }); @@ -19,9 +18,6 @@ class StreamChannelPage extends StatefulWidget { /// Initial scroll alignment for the message list. final double? initialAlignment; - /// Whether to highlight the initial message. - final bool highlightInitialMessage; - /// Callback for when the back button is pressed. final VoidCallback? onBackPressed; @@ -92,7 +88,6 @@ class _StreamChannelPageState extends State { body: _ChannelPageBody( initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, - highlightInitialMessage: widget.highlightInitialMessage, onReply: _reply, onEditMessage: _editMessage, typingIndicator: typingIndicator, @@ -114,7 +109,6 @@ class _ChannelPageBody extends StatelessWidget { required this.onEditMessage, this.initialScrollIndex, this.initialAlignment, - this.highlightInitialMessage = false, }); final Widget typingIndicator; @@ -122,7 +116,6 @@ class _ChannelPageBody extends StatelessWidget { final void Function(Message) onEditMessage; final int? initialScrollIndex; final double? initialAlignment; - final bool highlightInitialMessage; @override Widget build(BuildContext context) { @@ -133,10 +126,6 @@ class _ChannelPageBody extends StatelessWidget { StreamMessageListView( initialScrollIndex: initialScrollIndex, initialAlignment: initialAlignment, - config: StreamMessageListViewConfiguration( - highlightInitialMessage: highlightInitialMessage, - swipeToReply: true, - ), onEditMessageTap: onEditMessage, onReplyTap: onReply, threadBuilder: (_, parentMessage) { diff --git a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart index a68d4cfdd3..00050d4d42 100644 --- a/packages/stream_chat_flutter/lib/src/channel/thread_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/thread_page.dart @@ -115,11 +115,6 @@ class _ThreadBody extends StatelessWidget { initialAlignment: initialAlignment, onReplyTap: onReply, onEditMessageTap: onEditMessageTap, - config: const StreamMessageListViewConfiguration( - swipeToReply: true, - showScrollToBottom: false, - highlightInitialMessage: true, - ), onViewInChannelTap: onViewInChannelTap, topPadding: insets.topPadding, bottomPadding: insets.bottomPadding, diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index f9c9f722ac..d4d02f904f 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -123,7 +123,7 @@ class StreamMessageListView extends StatefulWidget { this.onEphemeralMessageTap, this.onModeratedMessageTap, this.onMessageLongPress, - this.config = const StreamMessageListViewConfiguration(), + this.config, this.builders = const StreamMessageListViewBuilders(), this.topPadding = 0, this.bottomPadding = 0, @@ -249,8 +249,10 @@ class StreamMessageListView extends StatefulWidget { /// [StreamMessageListViewConfiguration.markReadWhenAtTheBottom], scroll /// physics, and other non-builder, non-theme settings. /// - /// Defaults to [StreamMessageListViewConfiguration] with all defaults. - final StreamMessageListViewConfiguration config; + /// When null, falls back to + /// [StreamChatConfigurationData.messageListViewConfiguration] from the + /// nearest [StreamChatConfiguration] ancestor. + final StreamMessageListViewConfiguration? config; /// Custom slot builders for this message list view. /// @@ -324,6 +326,14 @@ class _StreamMessageListViewState extends State { MessageListController get _messageListController => widget.messageListController ?? _defaultController; + /// Returns the effective [StreamMessageListViewConfiguration] for this list. + /// + /// Uses [widget.config] when explicitly provided, otherwise falls back to + /// [StreamChatConfigurationData.messageListViewConfiguration] from the + /// nearest [StreamChatConfiguration] ancestor. + StreamMessageListViewConfiguration get _config => + widget.config ?? StreamChatConfiguration.of(context).messageListViewConfiguration; + StreamSubscription? _messageNewListener; StreamSubscription? _userReadListener; @@ -351,7 +361,7 @@ class _StreamMessageListViewState extends State { _unreadState.value = _readUnreadSnapshot(); - final highlightInitialMessage = widget.config.highlightInitialMessage; + final highlightInitialMessage = _config.highlightInitialMessage; final highlightMessageId = switch ((highlightInitialMessage, _isThreadConversation)) { (true, true) => _ThreadHighlightScope.of(context), (true, false) => streamChannel?.initialMessageId, @@ -547,9 +557,9 @@ class _StreamMessageListViewState extends State { child: Portal( child: ScaffoldMessenger( child: MessageListCore( - paginationLimit: widget.config.paginationLimit, - maximumMessageLimit: widget.config.maximumMessageLimit, - retentionTrimBuffer: widget.config.retentionTrimBuffer, + paginationLimit: _config.paginationLimit, + maximumMessageLimit: _config.maximumMessageLimit, + retentionTrimBuffer: _config.retentionTrimBuffer, messageFilter: widget.messageFilter, loadingBuilder: defaultLoadingBuilder, emptyBuilder: defaultEmptyBuilder, @@ -593,7 +603,7 @@ class _StreamMessageListViewState extends State { } return StreamInfoTile( - showMessage: widget.config.showConnectionStateTile && showStatus, + showMessage: _config.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, message: statusString, @@ -611,14 +621,14 @@ class _StreamMessageListViewState extends State { top: max(widget.topPadding, context.streamSpacing.sm), bottom: max(widget.bottomPadding, context.streamSpacing.sm), ), - keyboardDismissBehavior: widget.config.keyboardDismissBehavior, + keyboardDismissBehavior: _config.keyboardDismissBehavior, itemPositionsListener: _itemPositionListener, initialScrollIndex: initialIndex, initialAlignment: initialAlignment, - physics: widget.config.scrollPhysics, + physics: _config.scrollPhysics, itemScrollController: _scrollController, - reverse: widget.config.reverse, - shrinkWrap: widget.config.shrinkWrap, + reverse: _config.reverse, + shrinkWrap: _config.shrinkWrap, itemCount: itemCount, itemKeyBuilder: (index) { // Layout (see comment block below): indices 0/1 and the @@ -662,7 +672,7 @@ class _StreamMessageListViewState extends State { return ThreadSeparator(parentMessage: widget.parentMessage!); } if (i == itemCount - 3) { - if (widget.config.reverse ? widget.builders.header == null : widget.builders.footer == null) { + if (_config.reverse ? widget.builders.header == null : widget.builders.footer == null) { if (messages.isNotEmpty) { final message = messages.last; return _maybeBuildWithUnreadMessagesSeparator( @@ -676,7 +686,7 @@ class _StreamMessageListViewState extends State { return const SizedBox(height: 8); } if (i == 0) { - if (widget.config.reverse ? widget.builders.footer == null : widget.builders.header == null) { + if (_config.reverse ? widget.builders.footer == null : widget.builders.header == null) { return const Empty(); } return const SizedBox(height: 8); @@ -685,7 +695,7 @@ class _StreamMessageListViewState extends State { if (i == 1 || i == itemCount - 4) return const Empty(); late final Message message, nextMessage; - if (widget.config.reverse) { + if (_config.reverse) { message = messages[i - 1]; nextMessage = messages[i - 2]; } else { @@ -722,7 +732,7 @@ class _StreamMessageListViewState extends State { } if (i == itemCount - 2) { - if (widget.config.reverse) { + if (_config.reverse) { return widget.builders.header?.call(context) ?? const Empty(); } else { return widget.builders.footer?.call(context) ?? const Empty(); @@ -744,7 +754,7 @@ class _StreamMessageListViewState extends State { } if (i == 0) { - if (widget.config.reverse) { + if (_config.reverse) { return widget.builders.footer?.call(context) ?? const Empty(); } else { return widget.builders.header?.call(context) ?? const Empty(); @@ -763,13 +773,13 @@ class _StreamMessageListViewState extends State { ); }, ), - if (widget.config.showFloatingDateDivider) + if (_config.showFloatingDateDivider) Positioned( top: max(widget.topPadding, context.streamSpacing.sm), child: FloatingDateDivider( itemCount: itemCount, - reverse: widget.config.reverse, - fadeNearInlineDivider: widget.config.fadeFloatingDateDividerNearInline, + reverse: _config.reverse, + fadeNearInlineDivider: _config.fadeFloatingDateDividerNearInline, itemPositionListener: _itemPositionListener.itemPositions, messages: messages, dateDividerBuilder: switch (widget.builders.floatingDateDivider) { @@ -778,7 +788,7 @@ class _StreamMessageListViewState extends State { }, ), ), - if (widget.config.showScrollToBottom) + if (_config.showScrollToBottom) BetterStreamBuilder( stream: streamChannel!.channel.state!.isUpToDateStream, initialData: streamChannel!.channel.state!.isUpToDate, @@ -791,7 +801,7 @@ class _StreamMessageListViewState extends State { }, ), ), - if (widget.config.showUnreadIndicator && !_isThreadConversation) + if (_config.showUnreadIndicator && !_isThreadConversation) Positioned( top: max(widget.topPadding, context.streamSpacing.sm), child: UnreadIndicatorButton( @@ -989,7 +999,7 @@ class _StreamMessageListViewState extends State { Widget buildParentMessage(Message message) { final parentMessageProps = StreamMessageItemProps( message: message, - swipeToReply: widget.config.swipeToReply, + swipeToReply: _config.swipeToReply, onThreadTap: _onThreadTap, onMessageTap: widget.onMessageTap, onMessageLongPress: widget.onMessageLongPress, @@ -1042,14 +1052,14 @@ class _StreamMessageListViewState extends State { type: .outline, size: .medium, isFloating: true, - icon: switch (widget.config.reverse) { + icon: switch (_config.reverse) { true => Icon(context.streamIcons.arrowDown), false => Icon(context.streamIcons.arrowUp), }, onPressed: () => scrollToBottomDefaultTapAction(unreadCount), ); - if (showUnreadCount && widget.config.showUnreadCountOnScrollToBottom) { + if (showUnreadCount && _config.showUnreadCountOnScrollToBottom) { button = StreamBadgeNotification( label: '${unreadCount > 99 ? '99+' : unreadCount}', child: button, @@ -1110,7 +1120,7 @@ class _StreamMessageListViewState extends State { final messageItemProps = StreamMessageItemProps( message: message, - swipeToReply: widget.config.swipeToReply, + swipeToReply: _config.swipeToReply, onThreadTap: _onThreadTap, onViewInChannelTap: _isThreadConversation ? widget.onViewInChannelTap ?? (message) => Navigator.of(context).pop(message.id) @@ -1195,7 +1205,7 @@ class _StreamMessageListViewState extends State { _lastFullyVisibleMessage = newLastFullyVisibleMessage; // Mark messages as read if needed. - if (widget.config.markReadWhenAtTheBottom) { + if (_config.markReadWhenAtTheBottom) { _maybeMarkMessagesAsRead().ignore(); } } diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart index f8958c6ffa..6b1e1a072b 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart @@ -165,6 +165,8 @@ class StreamChatConfigurationData { List? attachmentBuilders, StreamReactionsType? reactionType, StreamReactionsPosition? reactionPosition, + StreamMessageListViewConfiguration messageListViewConfiguration = + const StreamMessageListViewConfiguration(), }) { return StreamChatConfigurationData._( reactionIconResolver: reactionIconResolver ?? const DefaultReactionIconResolver(), @@ -175,6 +177,7 @@ class StreamChatConfigurationData { attachmentBuilders: attachmentBuilders, reactionType: reactionType, reactionPosition: reactionPosition, + messageListViewConfiguration: messageListViewConfiguration, ); } @@ -185,6 +188,7 @@ class StreamChatConfigurationData { required this.messagePreviewFormatter, required this.imageCDN, required this.attachmentBuilders, + required this.messageListViewConfiguration, this.reactionType, this.reactionPosition, }); @@ -200,6 +204,7 @@ class StreamChatConfigurationData { List? attachmentBuilders, StreamReactionsType? reactionType, StreamReactionsPosition? reactionPosition, + StreamMessageListViewConfiguration? messageListViewConfiguration, }) { return StreamChatConfigurationData( reactionIconResolver: reactionIconResolver ?? this.reactionIconResolver, @@ -210,6 +215,7 @@ class StreamChatConfigurationData { attachmentBuilders: attachmentBuilders ?? this.attachmentBuilders, reactionType: reactionType ?? this.reactionType, reactionPosition: reactionPosition ?? this.reactionPosition, + messageListViewConfiguration: messageListViewConfiguration ?? this.messageListViewConfiguration, ); } @@ -258,4 +264,10 @@ class StreamChatConfigurationData { /// When null, the widget resolves its own default /// ([StreamReactionsPosition.header]). final StreamReactionsPosition? reactionPosition; + + /// The default [StreamMessageListViewConfiguration] applied to every + /// [StreamMessageListView] that does not provide its own explicit [config]. + /// + /// Defaults to [StreamMessageListViewConfiguration] with all defaults. + final StreamMessageListViewConfiguration messageListViewConfiguration; } diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index 6e69708419..c748278aef 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -258,6 +258,10 @@ extension on SampleAppConfigData { }, ), ], + messageListViewConfiguration: const StreamMessageListViewConfiguration( + highlightInitialMessage: true, + swipeToReply: true, + ), ); } } diff --git a/sample_app/lib/routes/app_routes.dart b/sample_app/lib/routes/app_routes.dart index 547ce90cfa..f593cf50ad 100644 --- a/sample_app/lib/routes/app_routes.dart +++ b/sample_app/lib/routes/app_routes.dart @@ -45,7 +45,6 @@ final appRoutes = [ return (parentMessage != null) ? StreamThreadPage(parent: parentMessage) : StreamChannelPage( - highlightInitialMessage: messageId != null, onChannelAvatarPressed: (context, channel) { final isOneToOne = channel.isOneToOne; final currentUserId = StreamChat.of(context).currentUser?.id; From 3ac8eb5cbeca1ab124c6bf9cd403f6b332383e12 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 15:04:20 +0200 Subject: [PATCH 25/48] Fix scroll button showing on thread --- .../message_list_view/message_list_view.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index d4d02f904f..f83dd7b8c8 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -789,18 +789,28 @@ class _StreamMessageListViewState extends State { ), ), if (_config.showScrollToBottom) - BetterStreamBuilder( - stream: streamChannel!.channel.state!.isUpToDateStream, - initialData: streamChannel!.channel.state!.isUpToDate, - builder: (context, snapshot) => ValueListenableBuilder( + if (_isThreadConversation) + ValueListenableBuilder( valueListenable: _showScrollToBottom, child: _buildScrollToBottom(), builder: (context, value, child) { - if (!snapshot || value) return child!; + if (value) return child!; return const Empty(); }, + ) + else + BetterStreamBuilder( + stream: streamChannel!.channel.state!.isUpToDateStream, + initialData: streamChannel!.channel.state!.isUpToDate, + builder: (context, snapshot) => ValueListenableBuilder( + valueListenable: _showScrollToBottom, + child: _buildScrollToBottom(), + builder: (context, value, child) { + if (!snapshot || value) return child!; + return const Empty(); + }, + ), ), - ), if (_config.showUnreadIndicator && !_isThreadConversation) Positioned( top: max(widget.topPadding, context.streamSpacing.sm), From cf907e788c41ee77912c245c26f99ff843bd3a50 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 15:37:02 +0200 Subject: [PATCH 26/48] Check if thread replies are enabled --- .../lib/src/message_action/message_actions_builder.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart b/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart index 5167670b0e..0f7213b90c 100644 --- a/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart +++ b/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart @@ -115,6 +115,7 @@ class StreamMessageActionsBuilder { final isParentMessage = (message.replyCount ?? 0) > 0; final canShowInChannel = message.showInChannel ?? true; final isPrivateMessage = message.hasRestrictedVisibility; + final repliesEnabled = channel.config?.replies ?? true; final canSendReply = channel.canSendReply; final canPinMessage = channel.canPinMessage; final canQuoteMessage = channel.canQuoteMessage; @@ -143,7 +144,7 @@ class StreamMessageActionsBuilder { // Thread reply action is only available for parent messages that are not in a // thread view, as replying in a thread that is already being viewed doesn't make sense. // Additionally, the channel needs to support sending replies. - if (canSendReply && !isThreadMessage && !isInThreadView) { + if (canSendReply && repliesEnabled && !isThreadMessage && !isInThreadView) { messageActions.add( StreamContextMenuAction( value: ThreadReply(message: message), From 3e477cc69c43fec8644b32d67809172b584ba165 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 15:37:13 +0200 Subject: [PATCH 27/48] fix background attachment picker --- .../attachment_picker/options/stream_gallery_picker.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart index 0fee3f8071..247956d7fa 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart @@ -75,7 +75,7 @@ class _StreamGalleryPickerState extends State { return FutureBuilder( future: requestPermission, builder: (context, snapshot) { - if (!snapshot.hasData) return const Empty(); + if (!snapshot.hasData) return const SizedBox.expand(); final spacing = context.streamSpacing; final textTheme = context.streamTextTheme; From 74888d12fff5298e85e58d39d368f86b486d31af Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 15:48:12 +0200 Subject: [PATCH 28/48] set default appstyle to regular --- sample_app/lib/config/sample_app_config.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample_app/lib/config/sample_app_config.dart b/sample_app/lib/config/sample_app_config.dart index 13d5fbf0c2..ca3e39b5df 100644 --- a/sample_app/lib/config/sample_app_config.dart +++ b/sample_app/lib/config/sample_app_config.dart @@ -52,7 +52,7 @@ class SampleAppConfigData { factory SampleAppConfigData({ Locale? locale, ThemeMode themeMode = .system, - SampleAppStyle appStyle = .floating, + SampleAppStyle appStyle = .regular, bool forceRtl = false, bool enableReminderActions = false, bool enableDeleteForMe = false, @@ -98,7 +98,7 @@ class SampleAppConfigData { /// Loads config from [StreamingSharedPreferences], falling back to defaults. factory SampleAppConfigData.fromPreferences(StreamingSharedPreferences prefs) { final localeStr = prefs.getString(_kLocale, defaultValue: '').getValue(); - final appStyleIndex = prefs.getInt(_kAppStyle, defaultValue: SampleAppStyle.floating.index).getValue(); + final appStyleIndex = prefs.getInt(_kAppStyle, defaultValue: SampleAppStyle.regular.index).getValue(); return SampleAppConfigData.raw( themeMode: ThemeMode.values[prefs.getInt(_kThemeMode, defaultValue: ThemeMode.system.index).getValue()], appStyle: SampleAppStyle.values[appStyleIndex.clamp(0, SampleAppStyle.values.length - 1)], From a67a7168e2e8d7fe49b2213d3bb220d1601d1b58 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 12 Jun 2026 17:41:20 +0200 Subject: [PATCH 29/48] fix review comments --- .../lib/src/channel/channel_header.dart | 3 ++- .../lib/src/channel/channel_list_header.dart | 3 ++- .../stream_chat_flutter/lib/src/channel/channel_page.dart | 8 ++++---- .../lib/src/message_input/stream_message_composer.dart | 8 +++----- .../stream_chat_flutter/lib/src/misc/back_button.dart | 3 ++- sample_app/lib/pages/new_group_chat_screen.dart | 2 ++ sample_app/lib/routes/app_routes.dart | 4 ++-- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index 89d7aaac20..a540d68a09 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -131,7 +131,8 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget /// [StreamChatThemeData.channelHeaderTheme]. final StreamAppBarStyle? style; - /// Whether the header is floating. + /// 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 diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart index 4e9198bde2..67b93d3849 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart @@ -118,7 +118,8 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi /// [StreamChatThemeData.channelListHeaderTheme]. final StreamAppBarStyle? style; - /// Whether the header is floating. + /// 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 diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index e3d18c4212..bfe3a29600 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -29,7 +29,7 @@ class StreamChannelPage extends StatefulWidget { } class _StreamChannelPageState extends State { - FocusNode? _focusNode; + late final FocusNode _focusNode; final _messageComposerController = StreamMessageComposerController(); @override @@ -40,21 +40,21 @@ class _StreamChannelPageState extends State { @override void dispose() { - _focusNode!.dispose(); + _focusNode.dispose(); super.dispose(); } void _reply(Message message) { _messageComposerController.quotedMessage = message; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _focusNode!.requestFocus(); + _focusNode.requestFocus(); }); } void _editMessage(Message message) { _messageComposerController.editMessage(message); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _focusNode!.requestFocus(); + _focusNode.requestFocus(); }); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 8f7e1eeaf7..99f170e730 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -11,9 +11,6 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; const _kCommandTrigger = '/'; const _kMentionTrigger = '@'; -/// Fixed height of the inline attachment picker body. -const _kPickerBodyHeight = 333.0; - /// Signature for the function that determines if a [matchedUri] should be /// previewed as an OG Attachment. typedef OgPreviewFilter = bool Function(Uri matchedUri, String messageText); @@ -430,6 +427,7 @@ class MessageComposerProps { TextCapitalization? textCapitalization, bool? autofocus, bool? autoCorrect, + ComposerLocation? composerLocation, }) { return MessageComposerProps( onMessageSent: onMessageSent ?? this.onMessageSent, @@ -466,6 +464,7 @@ class MessageComposerProps { textCapitalization: textCapitalization ?? this.textCapitalization, autofocus: autofocus ?? this.autofocus, autoCorrect: autoCorrect ?? this.autoCorrect, + composerLocation: composerLocation ?? this.composerLocation, ); } @@ -978,8 +977,7 @@ class DefaultStreamMessageComposerState extends State m.userId != currentUserId).user + final otherUser = (isOneToOne && currentUserId != null) + ? channelMembers.firstWhereOrNull((m) => m.userId != currentUserId)?.user : null; final router = GoRouter.of(context); From d1ad9e14f2ebee8ae9d273a34fe509ca20e97264 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 16 Jun 2026 16:06:46 +0200 Subject: [PATCH 30/48] update core dependency --- docs/docs_screenshots/pubspec.yaml | 2 +- melos.yaml | 2 +- packages/stream_chat_flutter/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index d1b301dc8c..2d1a0f1ea2 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: 6f8ba0c07a6ffbc6e0ea96c560f010cf48b8012b + ref: 682d15f4b23cccd4ca0943b07883fd80d686d8c0 path: packages/stream_core_flutter dev_dependencies: diff --git a/melos.yaml b/melos.yaml index 1b0b621311..883f817cc8 100644 --- a/melos.yaml +++ b/melos.yaml @@ -101,7 +101,7 @@ command: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: 6f8ba0c07a6ffbc6e0ea96c560f010cf48b8012b + ref: 682d15f4b23cccd4ca0943b07883fd80d686d8c0 path: packages/stream_core_flutter synchronized: ^3.4.0 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index eab51be6bc..b9346bf897 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: # ignore: invalid_dependency git: url: https://github.com/GetStream/stream-core-flutter - ref: 6f8ba0c07a6ffbc6e0ea96c560f010cf48b8012b + ref: 682d15f4b23cccd4ca0943b07883fd80d686d8c0 path: packages/stream_core_flutter svg_icon_widget: ^0.0.1 synchronized: ^3.4.0 From c44ba13fcdce2cd93284656ca8e51d57b98e0d8b Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 16 Jun 2026 16:18:46 +0200 Subject: [PATCH 31/48] update changelog --- packages/stream_chat_flutter/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index f8e94349f5..11292ccf53 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -2,8 +2,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`, and `ComposerLocation` from `stream_core_flutter` via `package:stream_chat_flutter/stream_chat_flutter.dart`. ⚠️ Deprecated From 0c62f89402e661fd33342f3c3ddb506508b0d8a0 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 17 Jun 2026 11:36:07 +0200 Subject: [PATCH 32/48] update core dependency --- docs/docs_screenshots/pubspec.yaml | 2 +- melos.yaml | 2 +- .../stream_chat_flutter/lib/src/channel/channel_header.dart | 1 - .../attachment_picker/options/stream_gallery_picker.dart | 1 - .../stream_chat_flutter/lib/src/stream_chat_configuration.dart | 3 +-- packages/stream_chat_flutter/pubspec.yaml | 2 +- 6 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index 2d1a0f1ea2..e0ad46a9d9 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: 682d15f4b23cccd4ca0943b07883fd80d686d8c0 + ref: ef83513b075246cb662911926ad49ada2a07f595 path: packages/stream_core_flutter dev_dependencies: diff --git a/melos.yaml b/melos.yaml index 883f817cc8..42dd5cd825 100644 --- a/melos.yaml +++ b/melos.yaml @@ -101,7 +101,7 @@ command: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: 682d15f4b23cccd4ca0943b07883fd80d686d8c0 + ref: ef83513b075246cb662911926ad49ada2a07f595 path: packages/stream_core_flutter synchronized: ^3.4.0 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index a540d68a09..6f0429250c 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -1,7 +1,6 @@ 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. diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart index 247956d7fa..0bd71c840c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart @@ -6,7 +6,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker.dart'; import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker_controller.dart'; -import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery.dart'; import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart'; import 'package:stream_chat_flutter/src/utils/utils.dart'; diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart index 6b1e1a072b..62c46a3ce1 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart @@ -165,8 +165,7 @@ class StreamChatConfigurationData { List? attachmentBuilders, StreamReactionsType? reactionType, StreamReactionsPosition? reactionPosition, - StreamMessageListViewConfiguration messageListViewConfiguration = - const StreamMessageListViewConfiguration(), + StreamMessageListViewConfiguration messageListViewConfiguration = const StreamMessageListViewConfiguration(), }) { return StreamChatConfigurationData._( reactionIconResolver: reactionIconResolver ?? const DefaultReactionIconResolver(), diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index b9346bf897..4d540a9e7e 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: # ignore: invalid_dependency git: url: https://github.com/GetStream/stream-core-flutter - ref: 682d15f4b23cccd4ca0943b07883fd80d686d8c0 + ref: ef83513b075246cb662911926ad49ada2a07f595 path: packages/stream_core_flutter svg_icon_widget: ^0.0.1 synchronized: ^3.4.0 From 738d676c63f07c5aa8f3f6431769ee6e53e10ad6 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 17 Jun 2026 11:54:01 +0200 Subject: [PATCH 33/48] fix(ui): channel page controller leak, shared fade helper, photo gallery nit - Fix StreamChannelPage disposing its StreamMessageComposerController in dispose(); aligns it with StreamThreadPage and prevents a resource leak. Align init style: late + super.initState() first, matching the thread page. - Replace inline LinearGradient in _buildFloatingComposerBand with the shared streamFloatingFade() helper from stream_core_flutter; re-export the helper via stream_chat_flutter.dart. - Drop redundant top: 0 from StreamPhotoGallery default padding. Co-Authored-By: Claude Opus 4.8 --- docs/docs_screenshots/pubspec.yaml | 2 +- melos.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 4 +++- .../lib/src/channel/channel_page.dart | 6 ++++-- .../message_input/stream_message_composer.dart | 16 +++++----------- .../photo_gallery/stream_photo_gallery.dart | 2 +- packages/stream_chat_flutter/pubspec.yaml | 2 +- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index e0ad46a9d9..a61a06cc5f 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: ef83513b075246cb662911926ad49ada2a07f595 + ref: bed95c6a61dce641a1807cf642ebba66494ca6ec path: packages/stream_core_flutter dev_dependencies: diff --git a/melos.yaml b/melos.yaml index 42dd5cd825..9c9015465a 100644 --- a/melos.yaml +++ b/melos.yaml @@ -101,7 +101,7 @@ command: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: ef83513b075246cb662911926ad49ada2a07f595 + ref: bed95c6a61dce641a1807cf642ebba66494ca6ec path: packages/stream_core_flutter synchronized: ^3.4.0 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 11292ccf53..96ec405c09 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -10,7 +10,7 @@ - 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`, and `ComposerLocation` from `stream_core_flutter` via `package:stream_chat_flutter/stream_chat_flutter.dart`. +- 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`. ⚠️ Deprecated @@ -19,6 +19,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)) diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index bfe3a29600..d05ffe1912 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -30,17 +30,19 @@ class StreamChannelPage extends StatefulWidget { class _StreamChannelPageState extends State { late final FocusNode _focusNode; - final _messageComposerController = StreamMessageComposerController(); + late final StreamMessageComposerController _messageComposerController; @override void initState() { - _focusNode = FocusNode(); super.initState(); + _focusNode = FocusNode(); + _messageComposerController = StreamMessageComposerController(); } @override void dispose() { _focusNode.dispose(); + _messageComposerController.dispose(); super.dispose(); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 99f170e730..59a14ca6b2 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/message_input/error_alert_sheet.dart'; import 'package:stream_chat_flutter/src/message_input/stream_chat_message_input.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' show streamFloatingFade; const _kCommandTrigger = '/'; const _kMentionTrigger = '@'; @@ -1002,17 +1003,10 @@ class DefaultStreamMessageComposerState extends State Date: Wed, 17 Jun 2026 16:06:48 +0200 Subject: [PATCH 34/48] Update appstyle theming --- .../lib/src/channel/channel_header.dart | 22 ++- .../lib/src/channel/channel_list_header.dart | 19 ++- .../avatar/stream_channel_avatar.dart | 10 +- .../components/avatar/stream_user_avatar.dart | 6 +- .../avatar/stream_user_avatar_group.dart | 6 +- .../stream_message_composer.dart | 20 +-- .../lib/src/misc/back_button.dart | 12 +- .../lib/src/theme/message_composer_theme.dart | 130 ++++++++++++++++++ .../theme/message_composer_theme.g.theme.dart | 83 +++++++++++ .../lib/src/theme/stream_chat_theme.dart | 9 ++ .../src/theme/stream_chat_theme.g.theme.dart | 9 ++ .../lib/src/theme/themes.dart | 1 + .../lib/stream_chat_flutter.dart | 1 - sample_app/lib/app.dart | 4 +- 14 files changed, 285 insertions(+), 47 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/theme/message_composer_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/message_composer_theme.g.theme.dart diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index 6f0429250c..2eae9517de 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -81,7 +81,6 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget this.trailing, this.primary = true, this.style, - this.appBarBehavior, }); /// Called when the default channel-avatar trailing is pressed. @@ -130,10 +129,6 @@ 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); @@ -144,7 +139,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget var leading = this.leading; if (leading == null && automaticallyImplyLeading) { - leading = StreamBackButton(showUnreadCount: true, appBarBehavior: appBarBehavior); + leading = const StreamBackButton(showUnreadCount: true); } var title = this.title; @@ -153,7 +148,11 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget var subtitle = this.subtitle; subtitle ??= StreamChannelInfo(channel: channel); - final showAvatarShadow = switch (appBarBehavior ?? StreamTheme.of(context).appStyle.appBarBehavior) { + final effectiveAppBarBehavior = + style?.behavior ?? + StreamAppBarTheme.of(context).style?.behavior ?? + (StreamTheme.of(context).appStyle.isFloating ? AppBarBehavior.floating : AppBarBehavior.regular); + final showAvatarShadow = switch (effectiveAppBarBehavior) { .floating => true, .regular => false, }; @@ -162,7 +161,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget trailing ??= _DefaultChannelAvatar( channel: channel, onPressed: onChannelAvatarPressed, - showShadow: showAvatarShadow, + isFloating: showAvatarShadow, ); return Portal( @@ -201,7 +200,6 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget trailing: trailing, primary: primary, style: style, - appBarBehavior: appBarBehavior, ), ), ); @@ -212,11 +210,11 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget } class _DefaultChannelAvatar extends StatelessWidget { - const _DefaultChannelAvatar({required this.channel, this.onPressed, this.showShadow = false}); + const _DefaultChannelAvatar({required this.channel, this.onPressed, this.isFloating = false}); final Channel channel; final void Function(Channel channel)? onPressed; - final bool showShadow; + final bool isFloating; @override Widget build(BuildContext context) { @@ -237,7 +235,7 @@ class _DefaultChannelAvatar extends StatelessWidget { child: StreamChannelAvatar( size: .lg, channel: channel, - showShadow: showShadow, + isFloating: isFloating, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart index 67b93d3849..3ec71a454b 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart @@ -80,7 +80,6 @@ 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. @@ -118,10 +117,6 @@ 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); @@ -130,12 +125,16 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi final _client = client ?? StreamChat.of(context).client; final headerTheme = StreamChatTheme.of(context).channelListHeaderTheme; - final hasAvatarShadow = switch (appBarBehavior ?? StreamTheme.of(context).appStyle.appBarBehavior) { + final effectiveAppBarBehavior = + style?.behavior ?? + StreamAppBarTheme.of(context).style?.behavior ?? + (StreamTheme.of(context).appStyle.isFloating ? AppBarBehavior.floating : AppBarBehavior.regular); + final hasAvatarShadow = switch (effectiveAppBarBehavior) { .floating => true, .regular => false, }; - final leading = _DefaultUserAvatar(client: _client, onPressed: onUserAvatarPressed, showShadow: hasAvatarShadow); + final leading = _DefaultUserAvatar(client: _client, onPressed: onUserAvatarPressed, isFloating: hasAvatarShadow); return Portal( child: StreamConnectionStatusBuilder( @@ -189,11 +188,11 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi } class _DefaultUserAvatar extends StatelessWidget { - const _DefaultUserAvatar({required this.client, this.onPressed, this.showShadow = false}); + const _DefaultUserAvatar({required this.client, this.onPressed, this.isFloating = false}); final StreamChatClient client; final void Function(User user)? onPressed; - final bool showShadow; + final bool isFloating; @override Widget build(BuildContext context) { @@ -222,7 +221,7 @@ class _DefaultUserAvatar extends StatelessWidget { size: .lg, user: user, showOnlineIndicator: false, - showShadow: showShadow, + isFloating: isFloating, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart index 8ac6723233..2a4aa7c7e4 100644 --- a/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart @@ -54,7 +54,7 @@ class StreamChannelAvatar extends StatelessWidget { super.key, this.size, required this.channel, - this.showShadow = false, + this.isFloating, }); /// The channel whose avatar is displayed. @@ -70,7 +70,7 @@ class StreamChannelAvatar extends StatelessWidget { /// Defaults to false. The shadow style is determined by /// [StreamAvatarThemeData.boxShadow], falling back to /// [StreamBoxShadow.elevation3]. - final bool showShadow; + final bool? isFloating; @override Widget build(BuildContext context) { @@ -84,7 +84,7 @@ class StreamChannelAvatar extends StatelessWidget { builder: (context, channelImage) => StreamAvatar( imageUrl: channelImage, size: _avatarSizeForAvatarGroupSize(effectiveSize), - showShadow: showShadow, + isFloating: isFloating, placeholder: (_) => const _StreamChannelAvatarPlaceholder(), ), noDataBuilder: (context) => BetterStreamBuilder( @@ -104,14 +104,14 @@ class StreamChannelAvatar extends StatelessWidget { size: _avatarSizeForAvatarGroupSize(effectiveSize), // TODO: make this configurable when the online state is shown. showOnlineIndicator: otherUser.online, - showShadow: showShadow, + isFloating: isFloating, ); } return StreamUserAvatarGroup( size: effectiveSize, users: users.sortedBy((it) => it.id == currentUserId ? 1 : 0), - showShadow: showShadow, + isFloating: isFloating, ); }, ), diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart index 747ae2b5f0..ee27cb9b47 100644 --- a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart @@ -68,7 +68,7 @@ class StreamUserAvatar extends StatelessWidget { required this.user, this.showBorder = true, this.showOnlineIndicator = true, - this.showShadow = false, + this.isFloating, }); /// The user whose avatar is displayed. @@ -90,7 +90,7 @@ class StreamUserAvatar extends StatelessWidget { /// Defaults to false. The shadow style is determined by /// [StreamAvatarThemeData.boxShadow], falling back to /// [StreamBoxShadow.elevation3]. - final bool showShadow; + final bool? isFloating; /// The size of the avatar. /// @@ -115,7 +115,7 @@ class StreamUserAvatar extends StatelessWidget { size: effectiveSize, imageUrl: user.image, showBorder: showBorder, - showShadow: showShadow, + isFloating: isFloating, backgroundColor: effectiveBackgroundColor, foregroundColor: effectiveForegroundColor, placeholder: (_) => _StreamUserAvatarPlaceholder(user: user, size: effectiveSize), diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart index aebd47c9ae..b9e295e375 100644 --- a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart @@ -55,7 +55,7 @@ class StreamUserAvatarGroup extends StatelessWidget { super.key, required this.users, this.size, - this.showShadow = false, + this.isFloating, }); /// The list of users whose avatars are displayed. @@ -71,7 +71,7 @@ class StreamUserAvatarGroup extends StatelessWidget { /// Defaults to false. The shadow style is determined by /// [StreamAvatarThemeData.boxShadow], falling back to /// [StreamBoxShadow.elevation3]. - final bool showShadow; + final bool? isFloating; @override Widget build(BuildContext context) { @@ -81,7 +81,7 @@ class StreamUserAvatarGroup extends StatelessWidget { (user) => StreamUserAvatar( user: user, showOnlineIndicator: false, - showShadow: showShadow, + isFloating: isFloating, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 59a14ca6b2..d219d04c2d 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -96,7 +96,7 @@ class StreamMessageComposer extends StatelessWidget { TextCapitalization textCapitalization = TextCapitalization.sentences, bool autofocus = false, bool autoCorrect = true, - ComposerLocation? composerLocation, + ComposerLocation? location, }) : props = .new( onMessageSent: onMessageSent, preMessageSending: preMessageSending, @@ -132,7 +132,7 @@ class StreamMessageComposer extends StatelessWidget { textCapitalization: textCapitalization, autofocus: autofocus, autoCorrect: autoCorrect, - composerLocation: composerLocation, + location: location, ); /// Creates a [StreamMessageComposer] from a pre-built [MessageComposerProps]. @@ -193,7 +193,7 @@ class MessageComposerProps { this.textCapitalization = TextCapitalization.sentences, this.autofocus = false, this.autoCorrect = true, - this.composerLocation, + this.location, }); /// Function called after sending the message. @@ -389,7 +389,7 @@ class MessageComposerProps { final bool autoCorrect; /// The location of the message composer. - final ComposerLocation? composerLocation; + final ComposerLocation? location; /// Returns a copy of this [MessageComposerProps] with the given fields /// replaced with new values. @@ -428,7 +428,7 @@ class MessageComposerProps { TextCapitalization? textCapitalization, bool? autofocus, bool? autoCorrect, - ComposerLocation? composerLocation, + ComposerLocation? location, }) { return MessageComposerProps( onMessageSent: onMessageSent ?? this.onMessageSent, @@ -465,7 +465,7 @@ class MessageComposerProps { textCapitalization: textCapitalization ?? this.textCapitalization, autofocus: autofocus ?? this.autofocus, autoCorrect: autoCorrect ?? this.autoCorrect, - composerLocation: composerLocation ?? this.composerLocation, + location: location ?? this.location, ); } @@ -798,7 +798,9 @@ class DefaultStreamMessageComposerState extends State context.streamIcons.chevronLeft, _ => context.streamIcons.arrowLeft, }; - final isFloating = switch (appBarBehavior ?? StreamTheme.of(context).appStyle.appBarBehavior) { + final effectiveAppBarBehavior = + appBarBehavior ?? + StreamAppBarTheme.of(context).style?.behavior ?? + (StreamTheme.of(context).appStyle.isFloating ? AppBarBehavior.floating : AppBarBehavior.regular); + final isFloating = switch (effectiveAppBarBehavior) { .floating => true, .regular => false, }; diff --git a/packages/stream_chat_flutter/lib/src/theme/message_composer_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_composer_theme.dart new file mode 100644 index 0000000000..3cc8c79412 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_composer_theme.dart @@ -0,0 +1,130 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +part 'message_composer_theme.g.theme.dart'; + +/// The placement of the message composer — floating above the keyboard or +/// docked at the bottom edge of the screen. +/// +/// When null on [StreamMessageComposerThemeData], the ambient [StreamAppStyle] +/// is used as a fallback — [StreamAppStyle.floating] maps to [floating] and +/// [StreamAppStyle.regular] maps to [docked]. +/// +/// See also: +/// +/// * [StreamMessageComposerThemeData.location], which carries this +/// value. +/// * [StreamAppStyle], the global app-wide style that acts as fallback. +enum ComposerLocation { + /// The composer floats above the on-screen keyboard with appropriate safe + /// area padding. + floating, + + /// The composer is docked at the bottom edge of the screen. + docked, +} + +/// Applies a message composer theme to descendant composer widgets. +/// +/// Wrap a subtree with [StreamMessageComposerTheme] to override the composer +/// location. Access the merged theme using [StreamMessageComposerTheme.of]. +/// +/// {@tool snippet} +/// +/// Override composer placement for a specific screen: +/// +/// ```dart +/// StreamMessageComposerTheme( +/// data: StreamMessageComposerThemeData( +/// location: ComposerLocation.floating, +/// ), +/// child: StreamChannel( +/// channel: channel, +/// child: ChannelPage(), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamMessageComposerThemeData], which describes the theme data. +/// * [StreamMessageComposerThemeData.location], the setting it holds. +class StreamMessageComposerTheme extends InheritedTheme { + /// Creates a message composer theme that controls descendant composers. + const StreamMessageComposerTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The message composer theme data for descendant widgets. + final StreamMessageComposerThemeData data; + + /// Returns the [StreamMessageComposerThemeData] merged from local and global + /// themes. + /// + /// Local values from the nearest [StreamMessageComposerTheme] ancestor take + /// precedence over global values from [StreamChatTheme.of]. + /// + /// This allows partial overrides — for example, overriding only + /// [StreamMessageComposerThemeData.location] in a subtree while + /// inheriting other properties from the global theme. + static StreamMessageComposerThemeData of(BuildContext context) { + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamChatTheme.of(context).messageComposerTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) => StreamMessageComposerTheme(data: data, child: child); + + @override + bool updateShouldNotify(StreamMessageComposerTheme oldWidget) => data != oldWidget.data; +} + +/// Theme data for customizing the message composer placement. +/// +/// All fields are nullable. When a field is null, the consuming widget falls +/// back to the ambient [StreamAppStyle]. +/// +/// {@tool snippet} +/// +/// Override composer placement globally: +/// +/// ```dart +/// StreamChatThemeData( +/// messageComposerTheme: StreamMessageComposerThemeData( +/// location: ComposerLocation.floating, +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [ComposerLocation], the enum that describes the placement options. +/// * [StreamMessageComposerTheme], for overriding the theme in a subtree. +@themeGen +@immutable +class StreamMessageComposerThemeData with _$StreamMessageComposerThemeData { + /// Creates message composer theme data with optional overrides. + const StreamMessageComposerThemeData({this.location}); + + /// The placement of the message composer. + /// + /// When null the value falls back to the ambient [StreamAppStyle]: + /// [StreamAppStyle.floating] → [ComposerLocation.floating], + /// [StreamAppStyle.regular] → [ComposerLocation.docked]. + /// + /// Set this to override the global style for the composer only, without + /// affecting other components. + final ComposerLocation? location; + + /// Linearly interpolate between two [StreamMessageComposerThemeData] objects. + static StreamMessageComposerThemeData? lerp( + StreamMessageComposerThemeData? a, + StreamMessageComposerThemeData? b, + double t, + ) => _$StreamMessageComposerThemeData.lerp(a, b, t); +} diff --git a/packages/stream_chat_flutter/lib/src/theme/message_composer_theme.g.theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_composer_theme.g.theme.dart new file mode 100644 index 0000000000..ddd6ea3c2e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_composer_theme.g.theme.dart @@ -0,0 +1,83 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'message_composer_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamMessageComposerThemeData { + bool get canMerge => true; + + static StreamMessageComposerThemeData? lerp( + StreamMessageComposerThemeData? a, + StreamMessageComposerThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamMessageComposerThemeData( + location: t < 0.5 ? a.location : b.location, + ); + } + + StreamMessageComposerThemeData copyWith({ + ComposerLocation? location, + }) { + final _this = (this as StreamMessageComposerThemeData); + + return StreamMessageComposerThemeData( + location: location ?? _this.location, + ); + } + + StreamMessageComposerThemeData merge(StreamMessageComposerThemeData? other) { + final _this = (this as StreamMessageComposerThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith(location: other.location); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamMessageComposerThemeData); + final _other = (other as StreamMessageComposerThemeData); + + return _other.location == _this.location; + } + + @override + int get hashCode { + final _this = (this as StreamMessageComposerThemeData); + + return Object.hash(runtimeType, _this.location); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index e487996ce0..e883a3f8d8 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -132,6 +132,7 @@ class StreamChatThemeData extends ThemeExtension with _$Str StreamAppBarThemeData? channelHeaderTheme, StreamAppBarThemeData? channelListHeaderTheme, StreamAppBarThemeData? threadHeaderTheme, + StreamMessageComposerThemeData? messageComposerTheme, StreamMessageListViewThemeData? messageListViewTheme, StreamPollCreatorThemeData? pollCreatorTheme, StreamPollInteractorThemeData? pollInteractorTheme, @@ -149,6 +150,9 @@ class StreamChatThemeData extends ThemeExtension with _$Str channelListHeaderTheme ??= const StreamAppBarThemeData(); threadHeaderTheme ??= const StreamAppBarThemeData(); + // Message composer + messageComposerTheme ??= const StreamMessageComposerThemeData(); + // Message list messageListViewTheme ??= const StreamMessageListViewThemeData(); @@ -170,6 +174,7 @@ class StreamChatThemeData extends ThemeExtension with _$Str channelHeaderTheme: channelHeaderTheme, channelListHeaderTheme: channelListHeaderTheme, threadHeaderTheme: threadHeaderTheme, + messageComposerTheme: messageComposerTheme, messageListViewTheme: messageListViewTheme, pollCreatorTheme: pollCreatorTheme, pollInteractorTheme: pollInteractorTheme, @@ -189,6 +194,7 @@ class StreamChatThemeData extends ThemeExtension with _$Str required this.channelHeaderTheme, required this.channelListHeaderTheme, required this.threadHeaderTheme, + required this.messageComposerTheme, required this.messageListViewTheme, required this.pollCreatorTheme, required this.pollInteractorTheme, @@ -211,6 +217,9 @@ class StreamChatThemeData extends ThemeExtension with _$Str /// The thread header app bar theme for this theme. final StreamAppBarThemeData threadHeaderTheme; + /// The message composer theme for this theme. + final StreamMessageComposerThemeData messageComposerTheme; + /// The message list view theme for this theme. final StreamMessageListViewThemeData messageListViewTheme; diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.g.theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.g.theme.dart index ee0012763b..105993cf0d 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.g.theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.g.theme.dart @@ -15,6 +15,7 @@ mixin _$StreamChatThemeData on ThemeExtension { StreamAppBarThemeData? channelHeaderTheme, StreamAppBarThemeData? channelListHeaderTheme, StreamAppBarThemeData? threadHeaderTheme, + StreamMessageComposerThemeData? messageComposerTheme, StreamMessageListViewThemeData? messageListViewTheme, StreamPollCreatorThemeData? pollCreatorTheme, StreamPollInteractorThemeData? pollInteractorTheme, @@ -34,6 +35,7 @@ mixin _$StreamChatThemeData on ThemeExtension { channelListHeaderTheme: channelListHeaderTheme ?? _this.channelListHeaderTheme, threadHeaderTheme: threadHeaderTheme ?? _this.threadHeaderTheme, + messageComposerTheme: messageComposerTheme ?? _this.messageComposerTheme, messageListViewTheme: messageListViewTheme ?? _this.messageListViewTheme, pollCreatorTheme: pollCreatorTheme ?? _this.pollCreatorTheme, pollInteractorTheme: pollInteractorTheme ?? _this.pollInteractorTheme, @@ -80,6 +82,11 @@ mixin _$StreamChatThemeData on ThemeExtension { other.threadHeaderTheme, t, )!, + messageComposerTheme: StreamMessageComposerThemeData.lerp( + _this.messageComposerTheme, + other.messageComposerTheme, + t, + )!, messageListViewTheme: StreamMessageListViewThemeData.lerp( _this.messageListViewTheme, other.messageListViewTheme, @@ -155,6 +162,7 @@ mixin _$StreamChatThemeData on ThemeExtension { return _other.channelHeaderTheme == _this.channelHeaderTheme && _other.channelListHeaderTheme == _this.channelListHeaderTheme && _other.threadHeaderTheme == _this.threadHeaderTheme && + _other.messageComposerTheme == _this.messageComposerTheme && _other.messageListViewTheme == _this.messageListViewTheme && _other.pollCreatorTheme == _this.pollCreatorTheme && _other.pollInteractorTheme == _this.pollInteractorTheme && @@ -178,6 +186,7 @@ mixin _$StreamChatThemeData on ThemeExtension { _this.channelHeaderTheme, _this.channelListHeaderTheme, _this.threadHeaderTheme, + _this.messageComposerTheme, _this.messageListViewTheme, _this.pollCreatorTheme, _this.pollInteractorTheme, diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart index ab4d9de545..d247f4d95c 100644 --- a/packages/stream_chat_flutter/lib/src/theme/themes.dart +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -1,3 +1,4 @@ +export 'message_composer_theme.dart'; export 'message_list_view_theme.dart'; export 'poll_card_style.dart'; export 'poll_comments_sheet_theme.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 03f5ded2d1..39cad44f2d 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -4,7 +4,6 @@ export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'package:stream_core_flutter/stream_core_flutter.dart' show StreamAppStyle, - ComposerLocation, AppBarBehavior, BottomBarBehavior, StreamScaffold, diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index c748278aef..ab5c0ac962 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -172,8 +172,8 @@ class _StreamChatSampleAppState extends State final config = context.sampleAppConfig; StreamAppStyle appStyle() => switch (config.appStyle) { - SampleAppStyle.regular => const StreamAppStyle.regular(), - SampleAppStyle.floating => const StreamAppStyle.floating(), + SampleAppStyle.regular => StreamAppStyle.regular, + SampleAppStyle.floating => StreamAppStyle.floating, }; ThemeData theme(Brightness brightness) => ThemeData( From faf7c8a79e006f2c6b4bbbac3d0896e52602ca32 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 17 Jun 2026 16:14:42 +0200 Subject: [PATCH 35/48] update core dependency --- docs/docs_screenshots/pubspec.yaml | 2 +- melos.yaml | 2 +- packages/stream_chat_flutter/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index a61a06cc5f..e7bf56c39b 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: bed95c6a61dce641a1807cf642ebba66494ca6ec + ref: a54e4c81f365d7b477f65b6ccdb3afec9c147cea path: packages/stream_core_flutter dev_dependencies: diff --git a/melos.yaml b/melos.yaml index 9c9015465a..367c9e6ec7 100644 --- a/melos.yaml +++ b/melos.yaml @@ -101,7 +101,7 @@ command: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: bed95c6a61dce641a1807cf642ebba66494ca6ec + ref: a54e4c81f365d7b477f65b6ccdb3afec9c147cea path: packages/stream_core_flutter synchronized: ^3.4.0 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index a72852440c..7803016cc9 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: # ignore: invalid_dependency git: url: https://github.com/GetStream/stream-core-flutter - ref: bed95c6a61dce641a1807cf642ebba66494ca6ec + ref: a54e4c81f365d7b477f65b6ccdb3afec9c147cea path: packages/stream_core_flutter svg_icon_widget: ^0.0.1 synchronized: ^3.4.0 From 38a0c596ffbdfce4f09e78e6ff7c7d4e9a9f49e2 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 19 Jun 2026 11:31:25 +0200 Subject: [PATCH 36/48] update core dependency --- docs/docs_screenshots/pubspec.yaml | 2 +- melos.yaml | 2 +- .../lib/src/channel/channel_header.dart | 2 +- .../lib/src/channel/channel_list_header.dart | 2 +- .../lib/src/message_input/stream_message_composer.dart | 8 ++++---- .../stream_chat_flutter/lib/src/misc/back_button.dart | 4 ++-- packages/stream_chat_flutter/pubspec.yaml | 2 +- sample_app/lib/app.dart | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index 6273fad8f7..00a29c2e1d 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: f94d9ccf3f6cec2c098ec214b1c39095ddec907c + ref: ee041cd2cd63868ea3f02f8e902a631cf748d02a path: packages/stream_core_flutter dev_dependencies: diff --git a/melos.yaml b/melos.yaml index a0d7d1b543..7b9843cff0 100644 --- a/melos.yaml +++ b/melos.yaml @@ -101,7 +101,7 @@ command: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: f94d9ccf3f6cec2c098ec214b1c39095ddec907c + ref: ee041cd2cd63868ea3f02f8e902a631cf748d02a path: packages/stream_core_flutter synchronized: ^3.4.0 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index 2eae9517de..0950f2fc5e 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -151,7 +151,7 @@ class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget final effectiveAppBarBehavior = style?.behavior ?? StreamAppBarTheme.of(context).style?.behavior ?? - (StreamTheme.of(context).appStyle.isFloating ? AppBarBehavior.floating : AppBarBehavior.regular); + (StreamTheme.of(context).appStyle.isFloating ? .floating : .regular); final showAvatarShadow = switch (effectiveAppBarBehavior) { .floating => true, .regular => false, diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart index 3ec71a454b..e4729011ae 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart @@ -128,7 +128,7 @@ class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWi final effectiveAppBarBehavior = style?.behavior ?? StreamAppBarTheme.of(context).style?.behavior ?? - (StreamTheme.of(context).appStyle.isFloating ? AppBarBehavior.floating : AppBarBehavior.regular); + (StreamTheme.of(context).appStyle.isFloating ? .floating : .regular); final hasAvatarShadow = switch (effectiveAppBarBehavior) { .floating => true, .regular => false, diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 69e19b4dd1..63f323a2a9 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -1050,7 +1050,7 @@ class DefaultStreamMessageComposerState extends State true, .regular => false, diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index bc580f578e..da19df228b 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter - ref: f94d9ccf3f6cec2c098ec214b1c39095ddec907c + ref: ee041cd2cd63868ea3f02f8e902a631cf748d02a path: packages/stream_core_flutter svg_icon_widget: ^0.0.1 synchronized: ^3.4.0 diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index ab5c0ac962..5a72bbbadd 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -172,8 +172,8 @@ class _StreamChatSampleAppState extends State final config = context.sampleAppConfig; StreamAppStyle appStyle() => switch (config.appStyle) { - SampleAppStyle.regular => StreamAppStyle.regular, - SampleAppStyle.floating => StreamAppStyle.floating, + SampleAppStyle.regular => .regular, + SampleAppStyle.floating => .floating, }; ThemeData theme(Brightness brightness) => ThemeData( From 26d0f3f62dbbe5c1bc49c82d368962d928970266 Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:30:11 +0000 Subject: [PATCH 37/48] chore: Update Goldens --- .../goldens/macos/message_widget_actions.png | Bin 54686 -> 51929 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/docs_screenshots/test/message_list/goldens/macos/message_widget_actions.png b/docs/docs_screenshots/test/message_list/goldens/macos/message_widget_actions.png index daca885b8b9b3f580ce6633995cc83cf9d51367c..544a6d85cb7e75b2b6b8b8723485bcfa1a88b251 100644 GIT binary patch literal 51929 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

$Gjg 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≦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 LQiA#X+TZ>M*?!ur literal 54686 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 Date: Fri, 19 Jun 2026 15:10:00 +0200 Subject: [PATCH 38/48] update core and fix analysis --- docs/docs_screenshots/pubspec.yaml | 4 ++-- melos.yaml | 4 ++-- .../src/message_input/stream_message_composer.dart | 1 - packages/stream_chat_flutter/pubspec.yaml | 12 ++++++++++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index 00a29c2e1d..821db406d8 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -21,8 +21,8 @@ dependencies: stream_chat_flutter: ^10.0.1 stream_core_flutter: git: - url: https://github.com/GetStream/stream-core-flutter - ref: ee041cd2cd63868ea3f02f8e902a631cf748d02a + url: https://github.com/GetStream/stream-core-flutter.git + ref: 66ee511050b8ef4f8a39edf0c2c62568b521d34d path: packages/stream_core_flutter dev_dependencies: diff --git a/melos.yaml b/melos.yaml index 7b9843cff0..de07fca715 100644 --- a/melos.yaml +++ b/melos.yaml @@ -100,8 +100,8 @@ command: svg_icon_widget: ^0.0.1 stream_core_flutter: git: - url: https://github.com/GetStream/stream-core-flutter - ref: ee041cd2cd63868ea3f02f8e902a631cf748d02a + url: https://github.com/GetStream/stream-core-flutter.git + ref: 66ee511050b8ef4f8a39edf0c2c62568b521d34d path: packages/stream_core_flutter synchronized: ^3.4.0 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 63f323a2a9..7eae314b1b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/message_input/error_alert_sheet.dart'; import 'package:stream_chat_flutter/src/message_input/stream_chat_message_input.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_core_flutter/stream_core_flutter.dart' show streamFloatingFade; const _kCommandTrigger = '/'; const _kMentionTrigger = '@'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index da19df228b..9445f79c56 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -58,9 +58,17 @@ dependencies: shimmer: ^3.0.0 stream_chat_flutter_core: ^10.0.1 stream_core_flutter: + # The ignore below silences `invalid_dependency` because we occasionally + # pin stream_core_flutter to a git ref to iterate on it alongside this + # SDK between its releases. + # + # **Note:** Before publishing stream_chat_flutter, this MUST be swapped + # back to a pub version constraint — git deps are not allowed on pub.dev + # and will block the release. + # ignore: invalid_dependency git: - url: https://github.com/GetStream/stream-core-flutter - ref: ee041cd2cd63868ea3f02f8e902a631cf748d02a + url: https://github.com/GetStream/stream-core-flutter.git + ref: 66ee511050b8ef4f8a39edf0c2c62568b521d34d path: packages/stream_core_flutter svg_icon_widget: ^0.0.1 synchronized: ^3.4.0 From 6b1db2a04ef9c7c4fc046b2b2e1bce7531a059bd Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 19 Jun 2026 15:22:37 +0200 Subject: [PATCH 39/48] add option for reply in channel config --- docs/docs_screenshots/test/src/mocks.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs_screenshots/test/src/mocks.dart b/docs/docs_screenshots/test/src/mocks.dart index ed9c30b5e4..17979a5cdb 100644 --- a/docs/docs_screenshots/test/src/mocks.dart +++ b/docs/docs_screenshots/test/src/mocks.dart @@ -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); From 6593cc7978e3921cc0dbf874b211b38407007d8e Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:33:23 +0000 Subject: [PATCH 40/48] chore: Update Goldens --- .../goldens/macos/message_widget_actions.png | Bin 51929 -> 54686 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/docs_screenshots/test/message_list/goldens/macos/message_widget_actions.png b/docs/docs_screenshots/test/message_list/goldens/macos/message_widget_actions.png index 544a6d85cb7e75b2b6b8b8723485bcfa1a88b251..daca885b8b9b3f580ce6633995cc83cf9d51367c 100644 GIT binary patch literal 54686 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

$Gjg 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≦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 LQiA#X+TZ>M*?!ur From edd53f2bccdde7a6631371c621868d78463e29c5 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 19 Jun 2026 15:43:02 +0200 Subject: [PATCH 41/48] fix not using new api --- .../lib/src/message_input/stream_message_composer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 7eae314b1b..79f0c6eaed 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -1024,7 +1024,7 @@ class DefaultStreamMessageComposerState extends State Date: Fri, 19 Jun 2026 15:58:01 +0200 Subject: [PATCH 42/48] fix test and analysis --- .../lib/src/message_input/stream_message_composer.dart | 1 + .../test/src/message_action/message_actions_builder_test.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 79f0c6eaed..bd9085cdd6 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -1024,6 +1024,7 @@ class DefaultStreamMessageComposerState extends State customChannel.config).thenReturn(channelConfig); return customChannel; } From 1862ec01c755586875d66de36a555ee92ee36226 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 29 Jul 2026 11:09:18 +0200 Subject: [PATCH 43/48] project update --- .../ios/Runner.xcodeproj/project.pbxproj | 129 ++++-------------- 1 file changed, 24 insertions(+), 105 deletions(-) diff --git a/sample_app/ios/Runner.xcodeproj/project.pbxproj b/sample_app/ios/Runner.xcodeproj/project.pbxproj index 6fa58ae5c8..e1cace4f6a 100644 --- a/sample_app/ios/Runner.xcodeproj/project.pbxproj +++ b/sample_app/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 8210C58B8A5DAB805ACF46A1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4899F8CDC11D7148C6179369 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -51,6 +52,7 @@ 4899F8CDC11D7148C6179369 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 90B43020AC538F68084BBAFD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -69,6 +71,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 8210C58B8A5DAB805ACF46A1 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -87,6 +90,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -166,13 +170,15 @@ 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 0BC14C55242B5A7A0028DE94 /* Embed App Extensions */, 7BD3737C65F59B233B7F7FC6 /* [CP] Embed Pods Frameworks */, - 78D972C66518C545847BBB5A /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -202,6 +208,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -243,24 +252,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 78D972C66518C545847BBB5A /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/firebase_messaging/firebase_messaging_Privacy.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/firebase_messaging_Privacy.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 7BD3737C65F59B233B7F7FC6 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -268,97 +259,11 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", - "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCoreExtension/FirebaseCoreExtension.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCrashlytics/FirebaseCrashlytics.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseInstallations/FirebaseInstallations.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseRemoteConfigInterop/FirebaseRemoteConfigInterop.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseSessions/FirebaseSessions.framework", - "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", - "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", - "${BUILT_PRODUCTS_DIR}/PromisesSwift/Promises.framework", - "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", - "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", - "${BUILT_PRODUCTS_DIR}/audio_session/audio_session.framework", - "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", - "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", - "${BUILT_PRODUCTS_DIR}/file_selector_ios/file_selector_ios.framework", - "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", - "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", - "${BUILT_PRODUCTS_DIR}/flutter_secure_storage_darwin/flutter_secure_storage_darwin.framework", - "${BUILT_PRODUCTS_DIR}/gal/gal.framework", - "${BUILT_PRODUCTS_DIR}/geolocator_apple/geolocator_apple.framework", - "${BUILT_PRODUCTS_DIR}/get_thumbnail_video/get_thumbnail_video.framework", - "${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework", - "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", - "${BUILT_PRODUCTS_DIR}/just_audio/just_audio.framework", - "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/media_kit_video/media_kit_video.framework", - "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", - "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework", - "${BUILT_PRODUCTS_DIR}/record_ios/record_ios.framework", - "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", - "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", - "${BUILT_PRODUCTS_DIR}/sqflite_darwin/sqflite_darwin.framework", - "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", - "${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework", - "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", - "${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework", - "${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreExtension.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCrashlytics.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstallations.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseRemoteConfigInterop.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseSessions.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Promises.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audio_session.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_darwin.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/gal.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocator_apple.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/get_thumbnail_video.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/just_audio.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_video.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/record_ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite_darwin.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -718,6 +623,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } From 4490030ba9a02ba5f8a61cd60e6420fb4c2a716f Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 29 Jul 2026 11:56:55 +0200 Subject: [PATCH 44/48] fix pixel artifact between components --- .../stream_message_composer.dart | 76 +++++++++++++++++-- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 72e665fceb..e84da519d6 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -14,6 +14,17 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; const _kCommandTrigger = '/'; const _kMentionTrigger = '@'; +// How far the floating composer's lower background paints bleed *up*, under the +// paint above them. +// +// The floating composer stacks several same-colored fills (the pill's gradient +// band, the picker panel, the safe-area strip). Where two of them merely abut at +// a fractional device-pixel boundary, both edges get antialiased; composited in +// sequence their coverages don't sum to 1, so a hairline of whatever is behind +// (the message list) survives. Overlapping instead of abutting removes the seam, +// and is invisible because the paints share a color. +const _kFloatingSeamBleed = 1.0; + /// Signature for the function that determines if a [matchedUri] should be /// previewed as an OG Attachment. typedef OgPreviewFilter = bool Function(Uri matchedUri, String messageText); @@ -859,7 +870,9 @@ class DefaultStreamMessageComposerState extends State Date: Wed, 29 Jul 2026 14:10:36 +0200 Subject: [PATCH 45/48] change background into 1 gradient --- .../stream_message_composer.dart | 184 +++++++------ .../test/src/channel/channel_page_test.dart | 215 ++++++++++++++++ .../test/src/channel/thread_page_test.dart | 241 ++++++++++++++++++ .../message_composer_location_test.dart | 197 ++++++++++++++ .../theme/message_composer_theme_test.dart | 179 +++++++++++++ 5 files changed, 931 insertions(+), 85 deletions(-) create mode 100644 packages/stream_chat_flutter/test/src/channel/channel_page_test.dart create mode 100644 packages/stream_chat_flutter/test/src/channel/thread_page_test.dart create mode 100644 packages/stream_chat_flutter/test/src/message_input/message_composer_location_test.dart create mode 100644 packages/stream_chat_flutter/test/src/theme/message_composer_theme_test.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index e84da519d6..23110fb800 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -3,7 +3,9 @@ import 'dart:math' as math; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/semantics.dart'; +// Needed for RenderProxyBox; it also re-exports the semantics library that +// Assertiveness comes from, which is why flutter/semantics.dart isn't imported. +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/message_input/audio_recorder/audio_recorder_announcer.dart'; import 'package:stream_chat_flutter/src/message_input/composer_attachment_announcer.dart'; @@ -14,17 +16,6 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; const _kCommandTrigger = '/'; const _kMentionTrigger = '@'; -// How far the floating composer's lower background paints bleed *up*, under the -// paint above them. -// -// The floating composer stacks several same-colored fills (the pill's gradient -// band, the picker panel, the safe-area strip). Where two of them merely abut at -// a fractional device-pixel boundary, both edges get antialiased; composited in -// sequence their coverages don't sum to 1, so a hairline of whatever is behind -// (the message list) survives. Overlapping instead of abutting removes the seam, -// and is invisible because the paints share a color. -const _kFloatingSeamBleed = 1.0; - /// Signature for the function that determines if a [matchedUri] should be /// previewed as an OG Attachment. typedef OgPreviewFilter = bool Function(Uri matchedUri, String messageText); @@ -858,22 +849,20 @@ class DefaultStreamMessageComposerState extends State 0) { - final bandColor = context.streamColorScheme.backgroundElevation1; + // For floating: one continuous backdrop behind the pill, the picker and + // the safe-area zone. Painting it as a single layer is what keeps it + // seamless — abutting fills would each be antialiased at a fractional + // device-pixel boundary and let a hairline of the message list through. + if (effectiveComposerLocation == .floating) { return Stack( children: [ - Positioned( - bottom: 0, - left: 0, - right: 0, - // Bleeds up under the pill/picker above it to avoid a seam; - // this is a Positioned, so it only affects painting. - height: safeAreaPadding.bottom + _kFloatingSeamBleed, - child: ColoredBox(color: bandColor), + Positioned.fill( + child: CustomPaint( + painter: _FloatingComposerBackdropPainter( + color: context.streamColorScheme.backgroundElevation1, + fadeExtent: _pillHeight, + ), + ), ), Padding(padding: safeAreaPadding, child: child), ], @@ -1053,21 +1042,20 @@ class DefaultStreamMessageComposerState extends State _pillHeight.value = height, child: pill) + else + pill, ], ), ); @@ -1075,26 +1063,10 @@ class DefaultStreamMessageComposerState extends State(0); Widget _buildInlineAttachmentPicker(BuildContext context) { if (!_isPickerVisible) return const SizedBox.shrink(); @@ -1129,8 +1101,8 @@ class DefaultStreamMessageComposerState extends State fadeExtent; @override - Widget build(BuildContext context) { - // CustomPaint runs the painter before the child and never clips, which is - // what lets the fill escape above its own bounds. - return CustomPaint( - painter: _TopBleedFillPainter(color: color, bleed: _kFloatingSeamBleed), - child: child, + void paint(Canvas canvas, Size size) { + if (size.isEmpty) return; + + // The gradient runs bottom-to-top, so the solid run is measured from the + // bottom and the fade occupies the top `extent` pixels. + final extent = math.min(fadeExtent.value, size.height); + final solidFraction = (1 - extent / size.height).clamp(0.0, 1.0); + + final rect = Offset.zero & size; + final gradient = streamFloatingFadeLinearGradient( + color: color, + solidFraction: solidFraction, + begin: Alignment.bottomCenter, + end: Alignment.topCenter, ); + + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + } + + @override + bool shouldRepaint(_FloatingComposerBackdropPainter oldDelegate) { + return oldDelegate.color != color || oldDelegate.fadeExtent != fadeExtent; } } -class _TopBleedFillPainter extends CustomPainter { - const _TopBleedFillPainter({required this.color, required this.bleed}); +/// Reports [child]'s laid-out height via [onHeightChanged] during layout. +/// +/// Used to feed the pill's height to [_FloatingComposerBackdropPainter]. The +/// callback fires inside `performLayout`, which is safe for the listener to turn +/// into a repaint: the framework flushes all layout before any painting, so the +/// backdrop sees the current height in the same frame. +class _ReportHeight extends SingleChildRenderObjectWidget { + const _ReportHeight({required this.onHeightChanged, required super.child}); - final Color color; - final double bleed; + final ValueChanged onHeightChanged; @override - void paint(Canvas canvas, Size size) { - // Nothing to tuck under when collapsed; skip so we don't leave a stray line. - if (size.isEmpty) return; + _RenderReportHeight createRenderObject(BuildContext context) { + return _RenderReportHeight(onHeightChanged: onHeightChanged); + } - final rect = Rect.fromLTWH(0, -bleed, size.width, size.height + bleed); - canvas.drawRect(rect, Paint()..color = color); + @override + void updateRenderObject(BuildContext context, _RenderReportHeight renderObject) { + renderObject.onHeightChanged = onHeightChanged; } +} + +class _RenderReportHeight extends RenderProxyBox { + _RenderReportHeight({required this.onHeightChanged}); + + ValueChanged onHeightChanged; @override - bool shouldRepaint(_TopBleedFillPainter oldDelegate) { - return oldDelegate.color != color || oldDelegate.bleed != bleed; + void performLayout() { + super.performLayout(); + onHeightChanged(size.height); } } diff --git a/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart new file mode 100644 index 0000000000..86be8d77e7 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:record/record.dart'; +import 'package:stream_chat_flutter/src/message_input/stream_chat_message_input.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../fakes.dart'; +import '../mocks.dart'; + +void main() { + testWidgets('renders a header, a message list and a composer', (tester) async { + await _pumpChannelPage(tester); + + expect(find.byType(StreamChannelHeader), findsOneWidget); + expect(find.byType(StreamMessageListView), findsOneWidget); + expect(find.byType(StreamMessageComposer), findsOneWidget); + }); + + testWidgets('renders a typing indicator above the composer', (tester) async { + await _pumpChannelPage(tester, appStyle: StreamAppStyle.floating); + + final indicator = tester.getRect(_bodyTypingIndicator()); + final composer = tester.getRect(find.byType(StreamMessageComposer)); + + expect(indicator.bottom, lessThanOrEqualTo(composer.top)); + }); + + testWidgets('tapping the channel avatar invokes onChannelAvatarPressed', (tester) async { + Channel? pressedChannel; + await _pumpChannelPage(tester, onChannelAvatarPressed: (_, channel) => pressedChannel = channel); + + await tester.tap(_channelAvatarTapTarget()); + await tester.pumpAndSettle(); + + expect(pressedChannel, isNotNull); + }); + + testWidgets('replying to a message quotes it in the composer', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + + await _pumpChannelPage(tester); + _messageListView(tester).onReplyTap!(message); + await tester.pumpAndSettle(); + + expect(_composerController(tester).message.quotedMessage, message); + }); + + testWidgets('replying to a message focuses the composer', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + + await _pumpChannelPage(tester); + _messageListView(tester).onReplyTap!(message); + await tester.pumpAndSettle(); + + expect(_composerFocusNode(tester).hasFocus, isTrue); + }); + + testWidgets('editing a message loads it into the composer', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + + await _pumpChannelPage(tester); + _messageListView(tester).onEditMessageTap!(message); + await tester.pumpAndSettle(); + + expect(_composerController(tester).messageBeingEdited, message); + }); + + testWidgets('editing a message focuses the composer', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + + await _pumpChannelPage(tester); + _messageListView(tester).onEditMessageTap!(message); + await tester.pumpAndSettle(); + + expect(_composerFocusNode(tester).hasFocus, isTrue); + }); + + testWidgets('opens a thread page for the tapped parent message', (tester) async { + final parentMessage = Message(id: 'parent-id', text: 'Hello world!'); + + await _pumpChannelPage(tester); + final context = tester.element(find.byType(StreamMessageListView)); + final thread = _messageListView(tester).threadBuilder!(context, parentMessage); + + expect(thread, isA().having((it) => it.parent, 'parent', parentMessage)); + }); + + testWidgets('insets the message list behind a floating app bar and composer', (tester) async { + await _pumpChannelPage(tester, appStyle: StreamAppStyle.floating); + + final messageListView = _messageListView(tester); + + expect(messageListView.topPadding, greaterThan(0)); + expect(messageListView.bottomPadding, greaterThan(0)); + }); + + testWidgets('does not inset the message list when the app style is regular', (tester) async { + await _pumpChannelPage(tester); + + final messageListView = _messageListView(tester); + + expect(messageListView.topPadding, 0); + expect(messageListView.bottomPadding, 0); + }); + + testWidgets('disposes its composer controller when removed from the tree', (tester) async { + await _pumpChannelPage(tester); + final controller = _composerController(tester); + + await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink())); + + // A disposed ChangeNotifier throws when listened to again. + expect(() => controller.addListener(() {}), throwsFlutterError); + }); +} + +StreamMessageListView _messageListView(WidgetTester tester) { + return tester.widget(find.byType(StreamMessageListView)); +} + +/// The typing indicator the page renders in its body. +/// +/// The header renders one of its own as part of the channel subtitle, so the +/// plain type finder is ambiguous. +Finder _bodyTypingIndicator() { + final inHeader = find + .descendant(of: find.byType(StreamChannelHeader), matching: find.byType(StreamTypingIndicator)) + .evaluate() + .toSet(); + + return find.byElementPredicate((element) { + return element.widget is StreamTypingIndicator && !inHeader.contains(element); + }); +} + +/// The gesture detector wrapping the header's channel avatar. +Finder _channelAvatarTapTarget() { + return find.ancestor( + of: find.byType(StreamChannelAvatar), + matching: find.byType(GestureDetector), + ); +} + +/// The controller the page created and handed to its composer. +StreamMessageComposerController _composerController(WidgetTester tester) { + return tester.widget(find.byType(StreamChatMessageInput)).controller!; +} + +/// The focus node the page created and handed to its composer. +FocusNode _composerFocusNode(WidgetTester tester) { + return tester.widget(find.byType(StreamChatMessageInput)).focusNode!; +} + +Future _pumpChannelPage( + WidgetTester tester, { + StreamAppStyle appStyle = StreamAppStyle.regular, + void Function(BuildContext context, Channel channel)? onChannelAvatarPressed, +}) async { + final originalRecordPlatform = RecordPlatform.instance; + RecordPlatform.instance = FakeRecordPlatform(); + addTearDown(() => RecordPlatform.instance = originalRecordPlatform); + + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final currentUser = OwnUser(id: 'user-id'); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(currentUser); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(currentUser)); + when(() => clientState.totalUnreadCount).thenReturn(0); + when(() => clientState.totalUnreadCountStream).thenAnswer((_) => Stream.value(0)); + + when(() => channel.client).thenReturn(client); + when(() => channel.state).thenReturn(channelState); + when(channel.getRemainingCooldown).thenReturn(0); + when(() => channel.lastMessageAt).thenReturn(null); + when(() => channel.name).thenReturn('test'); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.image).thenReturn(null); + when(() => channel.imageStream).thenAnswer((_) => Stream.value(null)); + when(() => channel.isMuted).thenReturn(false); + when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); + when(() => channel.extraData).thenReturn({'name': 'test'}); + when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + + when(() => channelState.members).thenReturn([]); + when(() => channelState.membersStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.messages).thenReturn([]); + when(() => channelState.messagesStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.threadsStream).thenAnswer((_) => const Stream.empty()); + when(() => channelState.draft).thenReturn(null); + when(() => channelState.isUpToDateStream).thenAnswer((_) => Stream.value(true)); + when(() => channelState.unreadCountStream).thenAnswer((_) => Stream.value(0)); + when(() => channelState.readStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.currentUserRead).thenReturn(null); + when(() => channelState.currentUserReadStream).thenAnswer((_) => const Stream.empty()); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(extensions: [StreamTheme(appStyle: appStyle)]), + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: StreamChannelPage(onChannelAvatarPressed: onChannelAvatarPressed), + ), + ), + ), + ); + + await tester.pumpAndSettle(); +} diff --git a/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart b/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart new file mode 100644 index 0000000000..dd85333036 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart @@ -0,0 +1,241 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:record/record.dart'; +import 'package:stream_chat_flutter/src/message_input/stream_chat_message_input.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../fakes.dart'; +import '../mocks.dart'; + +void main() { + testWidgets('renders a header, a message list and a composer', (tester) async { + await _pumpThreadPage(tester); + + expect(find.byType(StreamThreadHeader), findsOneWidget); + expect(find.byType(StreamMessageListView), findsOneWidget); + expect(find.byType(StreamMessageComposer), findsOneWidget); + }); + + testWidgets('shows the thread of the parent message', (tester) async { + final parent = Message(id: 'parent-id', text: 'Hello world!'); + + await _pumpThreadPage(tester, parent: parent); + + expect(_messageListView(tester).parentMessage, parent); + }); + + testWidgets('addresses new messages to the parent thread', (tester) async { + final parent = Message(id: 'parent-id', text: 'Hello world!'); + + await _pumpThreadPage(tester, parent: parent); + + expect(_composerController(tester).message.parentId, 'parent-id'); + }); + + testWidgets('hides the composer when the parent message is deleted', (tester) async { + final parent = Message(id: 'parent-id', text: 'Hello world!', type: 'deleted'); + + await _pumpThreadPage(tester, parent: parent); + _ignoreDeletedBubbleOverflow(tester); + + expect(find.byType(StreamMessageComposer), findsNothing); + }); + + testWidgets('still shows the thread when the parent message is deleted', (tester) async { + final parent = Message(id: 'parent-id', text: 'Hello world!', type: 'deleted'); + + await _pumpThreadPage(tester, parent: parent); + _ignoreDeletedBubbleOverflow(tester); + + expect(find.byType(StreamMessageListView), findsOneWidget); + }); + + testWidgets('replying to a message quotes it in the composer', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + + await _pumpThreadPage(tester); + _messageListView(tester).onReplyTap!(message); + await tester.pumpAndSettle(); + + expect(_composerController(tester).message.quotedMessage, message); + }); + + testWidgets('replying to a message focuses the composer', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + + await _pumpThreadPage(tester); + _messageListView(tester).onReplyTap!(message); + await tester.pumpAndSettle(); + + expect(_composerFocusNode(tester).hasFocus, isTrue); + }); + + testWidgets('editing a message loads it into the composer', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + + await _pumpThreadPage(tester); + _messageListView(tester).onEditMessageTap!(message); + await tester.pumpAndSettle(); + + expect(_composerController(tester).messageBeingEdited, message); + }); + + testWidgets('editing a message focuses the composer', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + + await _pumpThreadPage(tester); + _messageListView(tester).onEditMessageTap!(message); + await tester.pumpAndSettle(); + + expect(_composerFocusNode(tester).hasFocus, isTrue); + }); + + testWidgets('forwards onViewInChannelTap to the message list', (tester) async { + final message = Message(id: 'message-id', text: 'Hello world!'); + Message? viewedInChannel; + + await _pumpThreadPage(tester, onViewInChannelTap: (message) => viewedInChannel = message); + _messageListView(tester).onViewInChannelTap!(message); + await tester.pumpAndSettle(); + + expect(viewedInChannel, message); + }); + + testWidgets('insets the message list behind a floating app bar and composer', (tester) async { + await _pumpThreadPage(tester, appStyle: StreamAppStyle.floating); + + final messageListView = _messageListView(tester); + + expect(messageListView.topPadding, greaterThan(0)); + expect(messageListView.bottomPadding, greaterThan(0)); + }); + + testWidgets('does not inset the message list when the app style is regular', (tester) async { + await _pumpThreadPage(tester); + + final messageListView = _messageListView(tester); + + expect(messageListView.topPadding, 0); + expect(messageListView.bottomPadding, 0); + }); + + testWidgets('disposes its composer controller when removed from the tree', (tester) async { + await _pumpThreadPage(tester); + final controller = _composerController(tester); + + await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink())); + + // A disposed ChangeNotifier throws when listened to again. + expect(() => controller.addListener(() {}), throwsFlutterError); + }); +} + +StreamMessageListView _messageListView(WidgetTester tester) { + return tester.widget(find.byType(StreamMessageListView)); +} + +/// Drains the overflow [FlutterError] the deleted-message bubble reports. +/// +/// `StreamMessageDeleted` lays out its icon and label in a fixed-width bubble. +/// The test font is squarer — and so wider — than the font that ships with the +/// app, which pushes the label a few pixels past the bubble. It fits in a real +/// app, so it is not what these tests are about. +void _ignoreDeletedBubbleOverflow(WidgetTester tester) { + final exception = tester.takeException(); + if (exception == null) return; + + expect(exception, isFlutterError); + expect('$exception', contains('overflowed')); +} + +/// The controller the page created and handed to its composer. +StreamMessageComposerController _composerController(WidgetTester tester) { + return tester.widget(find.byType(StreamChatMessageInput)).controller!; +} + +/// The focus node the page created and handed to its composer. +FocusNode _composerFocusNode(WidgetTester tester) { + return tester.widget(find.byType(StreamChatMessageInput)).focusNode!; +} + +Future _pumpThreadPage( + WidgetTester tester, { + Message? parent, + StreamAppStyle appStyle = StreamAppStyle.regular, + void Function(Message message)? onViewInChannelTap, +}) async { + final originalRecordPlatform = RecordPlatform.instance; + RecordPlatform.instance = FakeRecordPlatform(); + addTearDown(() => RecordPlatform.instance = originalRecordPlatform); + + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final currentUser = OwnUser(id: 'user-id'); + final parentMessage = parent ?? Message(id: 'parent-id', text: 'Hello world!'); + + // The thread is loaded — an empty reply list, not a missing one, is what + // takes the message list out of its skeleton-loading state. + final threads = {parentMessage.id: []}; + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(currentUser); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(currentUser)); + when(() => clientState.totalUnreadCount).thenReturn(0); + when(() => clientState.totalUnreadCountStream).thenAnswer((_) => Stream.value(0)); + when(() => clientState.channels).thenReturn({}); + when(() => clientState.channelsStream).thenAnswer((_) => Stream.value({})); + + when(() => channel.client).thenReturn(client); + when(() => channel.state).thenReturn(channelState); + when(channel.getRemainingCooldown).thenReturn(0); + when(() => channel.lastMessageAt).thenReturn(null); + when(() => channel.name).thenReturn('test'); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.image).thenReturn(null); + when(() => channel.imageStream).thenAnswer((_) => Stream.value(null)); + when(() => channel.isMuted).thenReturn(false); + when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); + when(() => channel.extraData).thenReturn({'name': 'test'}); + when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when( + () => channel.getReplies( + any(), + options: any(named: 'options'), + preferOffline: any(named: 'preferOffline'), + ), + ).thenAnswer((_) async => QueryRepliesResponse()..messages = []); + + when(() => channelState.members).thenReturn([]); + when(() => channelState.membersStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.messages).thenReturn([]); + when(() => channelState.messagesStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.threads).thenReturn(threads); + when(() => channelState.threadsStream).thenAnswer((_) => Stream.value(threads)); + when(() => channelState.draft).thenReturn(null); + when(() => channelState.isUpToDateStream).thenAnswer((_) => Stream.value(true)); + when(() => channelState.unreadCountStream).thenAnswer((_) => Stream.value(0)); + when(() => channelState.readStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.currentUserRead).thenReturn(null); + when(() => channelState.currentUserReadStream).thenAnswer((_) => const Stream.empty()); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(extensions: [StreamTheme(appStyle: appStyle)]), + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: StreamThreadPage( + parent: parentMessage, + onViewInChannelTap: onViewInChannelTap, + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); +} diff --git a/packages/stream_chat_flutter/test/src/message_input/message_composer_location_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_composer_location_test.dart new file mode 100644 index 0000000000..729142abac --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input/message_composer_location_test.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:record/record.dart'; +import 'package:stream_chat_flutter/src/message_input/stream_chat_message_input.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../fakes.dart'; +import '../mocks.dart'; + +void main() { + testWidgets('composer is docked when the app style is regular', (tester) async { + await _pumpComposer(tester, appStyle: StreamAppStyle.regular); + + expect(_resolvedLocation(tester), ComposerLocation.docked); + }); + + testWidgets('composer floats when the app style is floating', (tester) async { + await _pumpComposer(tester, appStyle: StreamAppStyle.floating); + + expect(_resolvedLocation(tester), ComposerLocation.floating); + }); + + testWidgets('the global composer theme overrides the app style', (tester) async { + await _pumpComposer( + tester, + appStyle: StreamAppStyle.regular, + globalTheme: const StreamMessageComposerThemeData(location: ComposerLocation.floating), + ); + + expect(_resolvedLocation(tester), ComposerLocation.floating); + }); + + testWidgets('a local composer theme overrides the global theme', (tester) async { + await _pumpComposer( + tester, + appStyle: StreamAppStyle.floating, + globalTheme: const StreamMessageComposerThemeData(location: ComposerLocation.floating), + localTheme: const StreamMessageComposerThemeData(location: ComposerLocation.docked), + ); + + expect(_resolvedLocation(tester), ComposerLocation.docked); + }); + + testWidgets('the location property overrides both the theme and the app style', (tester) async { + await _pumpComposer( + tester, + appStyle: StreamAppStyle.floating, + globalTheme: const StreamMessageComposerThemeData(location: ComposerLocation.floating), + location: ComposerLocation.docked, + ); + + expect(_resolvedLocation(tester), ComposerLocation.docked); + }); + + testWidgets('the docked composer fills its background with the elevation-1 color', (tester) async { + await _pumpComposer(tester, appStyle: StreamAppStyle.regular); + + expect(_backgroundFillFinder(tester), findsOneWidget); + }); + + testWidgets('the floating composer does not fill its background', (tester) async { + // Floating paints a fading backdrop instead of an opaque fill, so the + // message list stays visible behind the composer. + await _pumpComposer(tester, appStyle: StreamAppStyle.floating); + + expect(_backgroundFillFinder(tester), findsNothing); + }); + + testWidgets('the floating composer lifts the input above the bottom safe area', (tester) async { + const bottomInset = 34.0; + await _pumpComposer(tester, appStyle: StreamAppStyle.floating, bottomPadding: bottomInset); + + final gap = _gapBelowInput(tester); + + expect(gap, moreOrLessEquals(bottomInset)); + }); + + testWidgets('the floating composer keeps a minimum gap when there is no safe area', (tester) async { + await _pumpComposer(tester, appStyle: StreamAppStyle.floating); + + final gap = _gapBelowInput(tester); + + // Falls back to `spacing.md` so the pill never sits flush with the edge. + final context = tester.element(find.byType(StreamChatMessageInput)); + expect(gap, moreOrLessEquals(context.streamSpacing.md)); + }); + + testWidgets('the floating composer leaves no gap when the safe area is disabled', (tester) async { + await _pumpComposer( + tester, + appStyle: StreamAppStyle.floating, + bottomPadding: 34, + enableSafeArea: false, + ); + + final gap = _gapBelowInput(tester); + + expect(gap, moreOrLessEquals(0)); + }); +} + +/// The location the composer resolved, read back from the input it built. +ComposerLocation _resolvedLocation(WidgetTester tester) { + final input = tester.widget(find.byType(StreamChatMessageInput)); + return input.isFloating ? ComposerLocation.floating : ComposerLocation.docked; +} + +/// Finds the opaque background fill the docked composer paints behind itself. +Finder _backgroundFillFinder(WidgetTester tester) { + final context = tester.element(find.byType(StreamChatMessageInput)); + final fill = BoxDecoration(color: context.streamColorScheme.backgroundElevation1); + + return find.descendant( + of: find.byType(StreamMessageComposer), + matching: find.byWidgetPredicate((widget) => widget is DecoratedBox && widget.decoration == fill), + ); +} + +/// The vertical distance between the bottom of the input pill and the bottom +/// of the composer. +double _gapBelowInput(WidgetTester tester) { + final composer = tester.getRect(find.byType(StreamMessageComposer)); + final input = tester.getRect(find.byType(StreamChatMessageInput)); + + return composer.bottom - input.bottom; +} + +/// Pumps a [StreamMessageComposer] with the given placement inputs. +/// +/// The composer is placed at the bottom of the screen so its rect can be +/// compared against the input pill's. +Future _pumpComposer( + WidgetTester tester, { + required StreamAppStyle appStyle, + StreamMessageComposerThemeData? globalTheme, + StreamMessageComposerThemeData? localTheme, + ComposerLocation? location, + bool? enableSafeArea, + double bottomPadding = 0, +}) async { + final originalRecordPlatform = RecordPlatform.instance; + RecordPlatform.instance = FakeRecordPlatform(); + addTearDown(() => RecordPlatform.instance = originalRecordPlatform); + + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(OwnUser(id: 'user-id'))); + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(channel.getRemainingCooldown).thenReturn(0); + when(() => channel.lastMessageAt).thenReturn(null); + when(() => channel.extraData).thenReturn({'name': 'test'}); + when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channelState.members).thenReturn([]); + when(() => channelState.membersStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.messages).thenReturn([]); + when(() => channelState.messagesStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.draft).thenReturn(null); + + final composer = StreamMessageComposer( + location: location, + enableSafeArea: enableSafeArea, + ); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(extensions: [StreamTheme(appStyle: appStyle)]), + home: MediaQuery( + data: MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)), + child: StreamChat( + client: client, + themeData: StreamChatThemeData(messageComposerTheme: globalTheme), + child: StreamChannel( + channel: channel, + child: Scaffold( + body: Align( + alignment: Alignment.bottomCenter, + child: switch (localTheme) { + final localTheme? => StreamMessageComposerTheme(data: localTheme, child: composer), + _ => composer, + }, + ), + ), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); +} diff --git a/packages/stream_chat_flutter/test/src/theme/message_composer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_composer_theme_test.dart new file mode 100644 index 0000000000..b5b0a08f9d --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/message_composer_theme_test.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + test('copyWith with no arguments returns an equal MessageComposerThemeData', () { + const themeData = StreamMessageComposerThemeData(location: ComposerLocation.floating); + + expect(themeData.copyWith(), themeData); + }); + + test('copyWith with no arguments preserves the hashCode', () { + const themeData = StreamMessageComposerThemeData(location: ComposerLocation.floating); + + expect(themeData.copyWith().hashCode, themeData.hashCode); + }); + + test('copyWith overrides the location', () { + const docked = StreamMessageComposerThemeData(location: ComposerLocation.docked); + + expect(docked.copyWith(location: ComposerLocation.floating).location, ComposerLocation.floating); + }); + + test('MessageComposerThemeData instances with different locations are not equal', () { + expect( + const StreamMessageComposerThemeData(location: ComposerLocation.docked), + isNot(const StreamMessageComposerThemeData(location: ComposerLocation.floating)), + ); + }); + + test('lerp at t = 0 resolves to the start location', () { + expect( + StreamMessageComposerThemeData.lerp(_dockedTheme, _floatingTheme, 0)?.location, + ComposerLocation.docked, + ); + }); + + test('lerp below the halfway point resolves to the start location', () { + expect( + StreamMessageComposerThemeData.lerp(_dockedTheme, _floatingTheme, 0.49)?.location, + ComposerLocation.docked, + ); + }); + + test('lerp at or past the halfway point resolves to the end location', () { + expect( + StreamMessageComposerThemeData.lerp(_dockedTheme, _floatingTheme, 0.5)?.location, + ComposerLocation.floating, + ); + }); + + test('lerp at t = 1 resolves to the end location', () { + expect(StreamMessageComposerThemeData.lerp(_dockedTheme, _floatingTheme, 1), _floatingTheme); + }); + + test('merge with null keeps the original location', () { + expect(_dockedTheme.merge(null), _dockedTheme); + }); + + test('merge overrides the location with the other theme', () { + expect(_dockedTheme.merge(_floatingTheme), _floatingTheme); + }); + + test('merge with an empty theme keeps the original location', () { + expect(_floatingTheme.merge(const StreamMessageComposerThemeData()), _floatingTheme); + }); + + testWidgets('of returns a null location when no theme is configured', (tester) async { + final context = await _pumpAndCaptureContext(tester); + + expect(StreamMessageComposerTheme.of(context).location, isNull); + }); + + testWidgets('of returns the global theme location when no local theme is present', (tester) async { + final context = await _pumpAndCaptureContext(tester, globalTheme: _floatingTheme); + + expect(StreamMessageComposerTheme.of(context).location, ComposerLocation.floating); + }); + + testWidgets('of merges the local theme over the global theme', (tester) async { + final context = await _pumpAndCaptureContext( + tester, + globalTheme: _dockedTheme, + localTheme: _floatingTheme, + ); + + expect(StreamMessageComposerTheme.of(context).location, ComposerLocation.floating); + }); + + testWidgets('of falls back to the global theme when the local theme sets no location', (tester) async { + final context = await _pumpAndCaptureContext( + tester, + globalTheme: _floatingTheme, + localTheme: const StreamMessageComposerThemeData(), + ); + + expect(StreamMessageComposerTheme.of(context).location, ComposerLocation.floating); + }); + + testWidgets('wrap re-establishes the theme in a detached subtree', (tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + // `wrap` is what InheritedTheme.captureAll calls to carry the theme + // across a route boundary; assert it round-trips the same data. + final theme = StreamMessageComposerTheme( + data: _floatingTheme, + child: Builder( + builder: (context) { + capturedContext = context; + return const SizedBox.shrink(); + }, + ), + ); + + return theme.wrap(context, theme.child); + }, + ), + ), + ); + + expect(StreamMessageComposerTheme.of(capturedContext).location, ComposerLocation.floating); + }); + + testWidgets('updateShouldNotify is true when the data changes', (tester) async { + const oldWidget = StreamMessageComposerTheme(data: _dockedTheme, child: SizedBox.shrink()); + const newWidget = StreamMessageComposerTheme(data: _floatingTheme, child: SizedBox.shrink()); + + expect(newWidget.updateShouldNotify(oldWidget), isTrue); + }); + + testWidgets('updateShouldNotify is false when the data is unchanged', (tester) async { + const oldWidget = StreamMessageComposerTheme(data: _dockedTheme, child: SizedBox.shrink()); + const newWidget = StreamMessageComposerTheme(data: _dockedTheme, child: SizedBox.shrink()); + + expect(newWidget.updateShouldNotify(oldWidget), isFalse); + }); +} + +const _dockedTheme = StreamMessageComposerThemeData(location: ComposerLocation.docked); +const _floatingTheme = StreamMessageComposerThemeData(location: ComposerLocation.floating); + +/// Pumps a [StreamChat] configured with [globalTheme], optionally wrapped in a +/// local [StreamMessageComposerTheme] carrying [localTheme], and returns a +/// context below both. +Future _pumpAndCaptureContext( + WidgetTester tester, { + StreamMessageComposerThemeData? globalTheme, + StreamMessageComposerThemeData? localTheme, +}) async { + late BuildContext capturedContext; + + final leaf = Builder( + builder: (context) { + capturedContext = context; + return const SizedBox.shrink(); + }, + ); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + themeData: StreamChatThemeData(messageComposerTheme: globalTheme), + child: child, + ), + home: switch (localTheme) { + final localTheme? => StreamMessageComposerTheme(data: localTheme, child: leaf), + _ => leaf, + }, + ), + ); + + return capturedContext; +} From 2fc96e37308958a16262724ee52aa2e276ba89ab Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 29 Jul 2026 14:21:08 +0200 Subject: [PATCH 46/48] Add fixes for channelpage and deleted message --- packages/stream_chat_flutter/CHANGELOG.md | 1 + .../lib/src/channel/channel_page.dart | 11 +++- .../components/stream_message_deleted.dart | 6 ++- .../test/src/channel/channel_page_test.dart | 53 ++++++++++++++++--- .../test/src/channel/thread_page_test.dart | 19 +------ 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 1dfe7f591a..b43db2e84c 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -17,6 +17,7 @@ - Fixed the default `StreamChannel` loading and error states not being themed or localized; `StreamChat` now installs themed, connection-aware defaults, overridable per `StreamChannel` or via `DefaultStreamChannelBuilders`. - 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 "Message deleted" bubble overflowing its maximum width when the localized label is long; the label now wraps instead. ## 10.2.0 diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart index d05ffe1912..e0b3fcb52e 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_page.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_page.dart @@ -18,7 +18,10 @@ class StreamChannelPage extends StatefulWidget { /// Initial scroll alignment for the message list. final double? initialAlignment; - /// Callback for when the back button is pressed. + /// 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. @@ -63,6 +66,12 @@ class _StreamChannelPageState extends State { @override Widget build(BuildContext context) { final appBar = StreamChannelHeader( + // Leaving this null keeps the header's own default back button, which + // pops the route. + leading: switch (widget.onBackPressed) { + final onBackPressed? => StreamBackButton(onPressed: onBackPressed, showUnreadCount: true), + _ => null, + }, onChannelAvatarPressed: (channel) => widget.onChannelAvatarPressed?.call(context, channel), ); diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_deleted.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_deleted.dart index 3e6a7bdeb0..ae0a3316f3 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_deleted.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_deleted.dart @@ -29,7 +29,11 @@ class StreamMessageDeleted extends StatelessWidget { mainAxisSize: .min, children: [ Icon(icons.noSign, size: 16), - core.StreamMessageText(padding: .zero, context.translations.messageDeletedLabel), + // Flexible so a long translation wraps inside the bubble instead of + // overflowing it — the bubble has a fixed maximum width. + Flexible( + child: core.StreamMessageText(padding: .zero, context.translations.messageDeletedLabel), + ), ], ), ); diff --git a/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart index 86be8d77e7..e317fb5d78 100644 --- a/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart @@ -36,6 +36,34 @@ void main() { expect(pressedChannel, isNotNull); }); + testWidgets('tapping back invokes onBackPressed', (tester) async { + var backPressed = 0; + await _pumpChannelPage(tester, onBackPressed: () => backPressed++); + + await tester.tap(find.byType(StreamBackButton)); + await tester.pumpAndSettle(); + + expect(backPressed, 1); + }); + + testWidgets('onBackPressed replaces the default pop', (tester) async { + await _pumpChannelPage(tester, onBackPressed: () {}, pushOntoARoute: true); + + await tester.tap(find.byType(StreamBackButton)); + await tester.pumpAndSettle(); + + expect(find.byType(StreamChannelPage), findsOneWidget); + }); + + testWidgets('pops the route when onBackPressed is not set', (tester) async { + await _pumpChannelPage(tester, pushOntoARoute: true); + + await tester.tap(find.byType(StreamBackButton)); + await tester.pumpAndSettle(); + + expect(find.byType(StreamChannelPage), findsNothing); + }); + testWidgets('replying to a message quotes it in the composer', (tester) async { final message = Message(id: 'message-id', text: 'Hello world!'); @@ -108,7 +136,8 @@ void main() { await _pumpChannelPage(tester); final controller = _composerController(tester); - await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink())); + // A bare widget, so the whole app subtree unmounts. + await tester.pumpWidget(const SizedBox.shrink()); // A disposed ChangeNotifier throws when listened to again. expect(() => controller.addListener(() {}), throwsFlutterError); @@ -156,6 +185,8 @@ Future _pumpChannelPage( WidgetTester tester, { StreamAppStyle appStyle = StreamAppStyle.regular, void Function(BuildContext context, Channel channel)? onChannelAvatarPressed, + VoidCallback? onBackPressed, + bool pushOntoARoute = false, }) async { final originalRecordPlatform = RecordPlatform.instance; RecordPlatform.instance = FakeRecordPlatform(); @@ -198,16 +229,26 @@ Future _pumpChannelPage( when(() => channelState.currentUserRead).thenReturn(null); when(() => channelState.currentUserReadStream).thenAnswer((_) => const Stream.empty()); + final page = StreamChannelPage( + onChannelAvatarPressed: onChannelAvatarPressed, + onBackPressed: onBackPressed, + ); + await tester.pumpWidget( MaterialApp( theme: ThemeData(extensions: [StreamTheme(appStyle: appStyle)]), - home: StreamChat( + // Chat context lives above the navigator so it survives a pop. + builder: (context, child) => StreamChat( client: client, - child: StreamChannel( - channel: channel, - child: StreamChannelPage(onChannelAvatarPressed: onChannelAvatarPressed), - ), + child: StreamChannel(channel: channel, child: child!), ), + // '/channel' seeds the stack with '/' underneath it, giving the back + // button something to pop to. + initialRoute: pushOntoARoute ? '/channel' : '/', + routes: { + '/': (_) => pushOntoARoute ? const Scaffold(body: SizedBox.shrink()) : page, + '/channel': (_) => page, + }, ), ); diff --git a/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart b/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart index dd85333036..381e737919 100644 --- a/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart @@ -37,7 +37,6 @@ void main() { final parent = Message(id: 'parent-id', text: 'Hello world!', type: 'deleted'); await _pumpThreadPage(tester, parent: parent); - _ignoreDeletedBubbleOverflow(tester); expect(find.byType(StreamMessageComposer), findsNothing); }); @@ -46,7 +45,6 @@ void main() { final parent = Message(id: 'parent-id', text: 'Hello world!', type: 'deleted'); await _pumpThreadPage(tester, parent: parent); - _ignoreDeletedBubbleOverflow(tester); expect(find.byType(StreamMessageListView), findsOneWidget); }); @@ -124,7 +122,8 @@ void main() { await _pumpThreadPage(tester); final controller = _composerController(tester); - await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink())); + // A bare widget, so the whole app subtree unmounts. + await tester.pumpWidget(const SizedBox.shrink()); // A disposed ChangeNotifier throws when listened to again. expect(() => controller.addListener(() {}), throwsFlutterError); @@ -135,20 +134,6 @@ StreamMessageListView _messageListView(WidgetTester tester) { return tester.widget(find.byType(StreamMessageListView)); } -/// Drains the overflow [FlutterError] the deleted-message bubble reports. -/// -/// `StreamMessageDeleted` lays out its icon and label in a fixed-width bubble. -/// The test font is squarer — and so wider — than the font that ships with the -/// app, which pushes the label a few pixels past the bubble. It fits in a real -/// app, so it is not what these tests are about. -void _ignoreDeletedBubbleOverflow(WidgetTester tester) { - final exception = tester.takeException(); - if (exception == null) return; - - expect(exception, isFlutterError); - expect('$exception', contains('overflowed')); -} - /// The controller the page created and handed to its composer. StreamMessageComposerController _composerController(WidgetTester tester) { return tester.widget(find.byType(StreamChatMessageInput)).controller!; From a9ad64c5cdb45f544d75efc2b1b77c28fe42e34d Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 29 Jul 2026 14:54:36 +0200 Subject: [PATCH 47/48] Add snapshot tests for deleted message --- packages/stream_chat_flutter/CHANGELOG.md | 24 +-- .../stream_message_deleted_test.dart | 163 ++++++++++++++++++ 2 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 packages/stream_chat_flutter/test/src/message_widget/stream_message_deleted_test.dart diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index b43db2e84c..bc49143f7d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,7 +1,20 @@ ## 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. + ✅ 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 `ComposerLocation` 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 `appBarBehavior` parameter to `StreamChannelHeader`, `StreamChannelListHeader`, and `StreamBackButton`, controlling floating vs pinned app bar appearance (avatar shadow, back-button style). Falls back to `StreamAppBarTheme`, and then to the ambient `StreamAppStyle`, 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. +- 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. @@ -58,15 +71,6 @@ ✅ 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`). @@ -88,8 +92,6 @@ 🐞 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)) diff --git a/packages/stream_chat_flutter/test/src/message_widget/stream_message_deleted_test.dart b/packages/stream_chat_flutter/test/src/message_widget/stream_message_deleted_test.dart new file mode 100644 index 0000000000..8fa37a5b75 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_widget/stream_message_deleted_test.dart @@ -0,0 +1,163 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_deleted.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + // The bubble is capped at the message item's max width, so the label has to + // wrap rather than run past it. Guards the `Flexible` around the label. + testWidgets('lays out a long label without overflowing', (tester) async { + await tester.pumpWidget(_wrapWithApp(_deletedBubble(_longLabel))); + + expect(tester.takeException(), isNull); + }); + + testWidgets('lays out the default label without overflowing', (tester) async { + await tester.pumpWidget(_wrapWithApp(_deletedBubble(_shortLabel))); + + expect(tester.takeException(), isNull); + }); + + testWidgets('wraps a long label onto more than one line', (tester) async { + await tester.pumpWidget(_wrapWithApp(_deletedBubble(_shortLabel))); + final singleLine = tester.getSize(find.byType(StreamMessageDeleted)).height; + + await tester.pumpWidget(_wrapWithApp(_deletedBubble(_longLabel))); + final wrapped = tester.getSize(find.byType(StreamMessageDeleted)).height; + + expect(wrapped, greaterThan(singleLine)); + }); + + testWidgets('keeps a long label within the bubble width', (tester) async { + await tester.pumpWidget(_wrapWithApp(_deletedBubble(_longLabel))); + + final width = tester.getSize(find.byType(StreamMessageDeleted)).width; + + expect(width, lessThanOrEqualTo(_bubbleMaxWidth)); + }); + + for (final brightness in Brightness.values) { + goldenTest( + '[${brightness.name}] -> StreamMessageDeleted looks fine', + fileName: 'stream_message_deleted_${brightness.name}', + constraints: const BoxConstraints.tightFor(width: 460, height: 420), + // The app is themed once around the whole group; the scenarios below + // vary only the label. + builder: () => _wrapWithApp( + brightness: brightness, + GoldenTestGroup( + columns: 1, + children: [ + GoldenTestScenario( + name: 'very short label — bubble hugs it', + child: _deletedBubble(_tinyLabel, maxWidth: _goldenBubbleMaxWidth), + ), + GoldenTestScenario( + name: 'short label', + child: _deletedBubble(_shortLabel, maxWidth: _goldenBubbleMaxWidth), + ), + GoldenTestScenario( + name: 'longest shipped translation', + child: _deletedBubble(_longestShippedLabel, maxWidth: _goldenBubbleMaxWidth), + ), + GoldenTestScenario( + name: 'long label wraps inside the bubble', + child: _deletedBubble(_longLabel, maxWidth: _goldenBubbleMaxWidth), + ), + ], + ), + ), + ); + } +} + +/// Mirrors the default `StreamMessageItemProps.maxWidth`, which is what caps +/// the bubble in a real message list. Used by the layout tests, so they pin +/// the real geometry. +const _bubbleMaxWidth = 272.0; + +/// The cap used by the goldens instead of [_bubbleMaxWidth]. +/// +/// Every glyph in the test font is a square of the font size, so text measures +/// far wider here than in a real app — at [_bubbleMaxWidth] even the English +/// label would wrap, which is not what users see. Widening the cap absorbs that +/// difference so the goldens show the line breaks a real font produces. +const _goldenBubbleMaxWidth = 350.0; + +/// Short enough that the bubble hugs it, showing it is not forced to full width. +const _tinyLabel = 'Del'; + +/// The shipped English label. +const _shortLabel = 'Message deleted'; + +/// The longest `messageDeletedLabel` in `stream_chat_localizations` that uses +/// Latin script, so the golden renders it without needing extra fonts. +const _longestShippedLabel = 'Messaggio eliminato'; + +/// Longer than any shipped translation, to show the wrapping behaviour. +const _longLabel = 'This message was deleted by a moderator'; + +/// A [StreamMessageDeleted] reading [label], capped at [maxWidth]. +Widget _deletedBubble(String label, {double maxWidth = _bubbleMaxWidth}) { + return Builder( + builder: (context) => Localizations.override( + context: context, + delegates: [_FixedLabelDelegate(label)], + child: Align( + alignment: Alignment.centerLeft, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: const StreamMessageDeleted(), + ), + ), + ), + ); +} + +Widget _wrapWithApp(Widget child, {Brightness brightness = Brightness.light}) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData( + brightness: brightness, + extensions: [StreamTheme(brightness: brightness)], + ), + home: Scaffold( + backgroundColor: brightness == Brightness.dark ? const Color(0xFF101418) : const Color(0xFFF7F7F8), + body: Center(child: child), + ), + ); +} + +/// Serves a [StreamChatLocalizations] whose `messageDeletedLabel` is fixed. +class _FixedLabelDelegate extends LocalizationsDelegate { + const _FixedLabelDelegate(this.label); + + final String label; + + @override + bool isSupported(Locale locale) => true; + + @override + SynchronousFuture load(Locale locale) { + return SynchronousFuture(_FixedLabelTranslations(label)); + } + + @override + bool shouldReload(_FixedLabelDelegate old) => old.label != label; +} + +/// A [StreamChatLocalizations] that only answers `messageDeletedLabel`. +/// +/// [StreamMessageDeleted] reads nothing else, so anything else reaching this +/// stub is a mistake worth failing on rather than quietly defaulting. +class _FixedLabelTranslations implements StreamChatLocalizations { + const _FixedLabelTranslations(this.messageDeletedLabel); + + @override + final String messageDeletedLabel; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} From f7ae533d670518001f97b7e2ccf56402906aca1d Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:03:50 +0000 Subject: [PATCH 48/48] chore: Update Goldens --- .../theming/goldens/macos/theming_red.png | Bin 57400 -> 57051 bytes .../ci/stream_message_deleted_dark.png | Bin 0 -> 4536 bytes .../ci/stream_message_deleted_light.png | Bin 0 -> 3908 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/stream_chat_flutter/test/src/message_widget/goldens/ci/stream_message_deleted_dark.png create mode 100644 packages/stream_chat_flutter/test/src/message_widget/goldens/ci/stream_message_deleted_light.png diff --git a/docs/docs_screenshots/test/theming/goldens/macos/theming_red.png b/docs/docs_screenshots/test/theming/goldens/macos/theming_red.png index 23a39523774d822b90a1b30e3be2a774cb014a7c..30d11ee982aab45695d963e9f80ac8081401ca5e 100644 GIT binary patch literal 57051 zcmeEu1yfvImo9-I!QI`1yEGOYg1bW!+}$-mfS^erxVtv)?!jpYbZ~cfhkNpVGe2Oy zJ5#r&>QdEo_c^Ceuf6utXFt2Ysw&H(Arm9Rz`&r%%Soxjz`O#(z`*4rApl37&n9Pp zr`N6$@|sA%pC6K01n~ZqtGcWN40xP$4+e%3MqWx>(<}XO$VI(lt3%}hm0 z`#BUJ6iWS`-Nh&OQ*&*)r8uN^aj!(|M0ujt_?2&a?fPd}SnBW~%7{xQqtHot%KW6U z*}Et4=s6w<Nbx>i zdQoeIV6V+;=2afhgj$g)ma<9B;HdjIFKoQ^XsUY*bC50aA|)?MJW!qCX5eF+u)mIM z;3SZCDnKP7iAh5zm$t3Uu7~oh^F(N<&0DA9pHBOv=1MW8;>Sp6&YUL(!LTI4jN+q2 zmfmj?hGJI4wYI(X^tO)K0%^d26jVbR^Sp)s*zpaI3|Gw!mx{=vGZmex5?Q56l;m17 z;C+cFkBKkMC3h{;PaIkUH3W_fb1Mm#YgZ~E=u8DwC*@itWm_e|hA3IMI;pB6eb#24 zU5$LH5Pz*7!eIdG)M^By%koMW7Pk8J>vUHN38ZS%UYlz>0~1s49diF&&!Jt5x$uhe z=$abrVf%(*bM)4XsVVIDkx`7SZ*%gCzfJwXjzqq224#3#%@gBp87QggP#AZ*N!DuS z<3S!;V8?Py2X@6Hd#re5UiIdE`V#KT3lj#Ez?JZny9r z)zXlJRRk9acYrVM&dW5uym^wPJ8&O9;c4O(pygqyFaW)^Hj0Rhy>g$CVqUFEa-}~= zt}KJ>OAQQ|@)-CT#qe3*B)UIV}%DN`F8@W1tnrR`}2>6s+ma zbU@qcX=i$KWAG}`3wL5fcD7P?aTCt=V})Q-!e%r?$M)1r)k)4L>s83^d>(;Je2Ms% z3)h+wjuY2Os( zyfoq7OE!-UWM(Yf1&qkNkBVxriM;=vbEQDg;MGr3qJ^O2GLSsNEA0!hK|AJj+AVN9 zncGP>w=1ACJATRxX>D+Enz%{8$20ua5;-fWrPY&{>u(gJ&A`6HLmszcG=Moq$;gwQ z;Lt9AyqpF+aq5jP4Vgl(S?vrTSPSUr7VzTEowf_jN7&`eCnU>|!TD4)IL>92X^Pul zL&9RREmxkez$Y%CKaKWqLi~OT+sh}A&A5wP0&M))ZpNF4m%cqG)#R+51Dgo#cWPQc zTCZ{~2h8)Y#@{(|{BC`2P&$}v+BP6XA0$o6M2xM_^EeOIx+c|Ea&jjXxR;k+D7{BWVYx6pcCBb=Y@1-OM3nIE~~*AN*`End-> z=ty}A!DoUij~0Q)Ugg=qCpttAhvsZ|+-GSuEm&As2+<{D#|vKPDvwg{k&BwPHj^b) zpJ2f?Y1gWAXTIe7BoI{4`H+!0iH-V3@mOE_1LVw= znxFW!Ppw1VFkJxDft2*Y&pG-f$yUy)1mRe_TR~f8~mT zCx7SHS!XficiqD9?PUzXZNO1vVtE**Lx}6K?4Uw*<+Prj z$GXLM%K-x%&;%QT1%3=GG=Za~B6JPcKx4ppNr1?;{Xf02KZ&Ci*( zC-&^m&;AV-u@MgZ0M!-^^dv_2^;S6SFs6}-&eoa8bW>E!_ctVkKp?sm+AObLB}dDk zBEb2iOrQky^hi7l+2RNKZWEd4NE^~VpYMpd_Fh48h_TlXO6!On8cB+#zxM9+eQeZF z*B`FVlNEG2r8H=Cw7l3JBNcR6J3(xCjo|$BdoV&pzU*vq7R^?#)#mOETE$8Z+K3A8 zO-GtY69|sxN1?nHq5YlULdrRTxwkB#? zd4B|VNhnZ0a9&@QSZm%ppJC4Dep*7NkX8>TSsM^pghocZ>7>VZMZ&{l|Eqw;R=D## zO3&}6Q*f+#M1M)d92sijNw(DDF3D~>%Ny0!CJgc{@s&#+P{oB0fkY@*%3fGC87dnp zTh<;+-iAveVqla0ZW1hzItq9^lg;!#7VI#rD>nDcXEgLa@V_iKUmtxL%j)fZ^6eB& zz~sxEt4*_?9JN3N7yaIhKIu6AU9b>wremzJF;`G4_F?0kJ)<%}0tP(U6Ib;VuOCLNcT(s!b){ZQ+R$5N7dYUaaR!XW%ElQKR zU}R=CeGZ0yJpPQR@4yh*(`TZRGeMWvj^Ap;CYp7{ImZy!>1x#r)kwAVw1Jzo{+1uj zJH3I8ZHrdX&%I(_J^5Zi$$V(N|8TB8in+2$LZS>FZFeOr;OU;%nuNagOI1XKzGPG+ zWC^ZZTcVd{v%L*TF`YwPqU;O&>({4cB#yKULTPbg_YsqmlL!eQmDGV;BFAd$f}71D zP4Ru`!xFZ4{dK#W;y|$Gd!&mVP&?a`py0-Ikmw5)-bWg$d z<#$$6Qd_32?demwxy0NT#L+T9xuwRmY>+e}FAhe1nbF=dvm9Bej`I>G7rnnVeLnp~ zGuCk=-zX+`^J+OJGF@YC^ltM>IBgJeaod8&kXrLZ|1{8tK~4a++3)#l8#Hy-_6945 zIlOMaFwrT5f5kve*gFqJKY=%JSDJOp#a;pgGSDMYkWamY86zc%*o?kLiwdP=Os{1Z zXTvao9rv!nQu8Vz_^9e#E$@V`skF52#wr?JXu0EupvDX33&yA2WxRzsk&$1`{}el~ z`NY+HxIJA{YS}4%+sPZ*y^+elS)=Cce72m2Xh|mctL>$2F~Mhs!*`aYBUvnqa?g9y z749A?VxnS?E8VS0=l8zUtqcSC4OROIjL+rW-F(XQUY7-PnV%hxf>XUTC#U;F8#5be z+03wkV_SC*JW8s48hK-yUZt3<+A~q*mWa<4UA@9hfUJE>AfNd|3_3VJ8WJeCo9tqJ zRU#jWa*=#DU)y9fHeDeI3@Fyjg3HBh?D(pO9~<;rbWGgH%|}FYQEbPfwH>U7M}S`w zIKk#&e?zW2|FKc$xt!n2gdfjdQp#(ryWz_TDv@yVC>)~P(PHx}+bjb;XRKOd-;RNE zNYcxX)v5@@A6zmq)`txwc!n8a(TbdeF%z+=PG6udV;djG4PNrgo4g$~+n-wwbZdQh zXF>F-xE)v?nG^!0_pT@J;uapX(UqqEwEHt#Dx3SVR{9(%C^OV60r9s399&FKE0D}v zrsMix-^e?1t(+y$6MSQGre6p>AQD-7kDGAQibk7&U z#>QkvhMoRd=fLlrFddVClr+bd61@hxR1@e0sd%0ZvqBM6=bErQE@zmRv$1u~p^2bE zuF-*)As(+{(Enz6q}8!=iDg0me9sqmGxJ)X&+V2F7Pdk|$Re8Dtz$u`a`pWYzp%Pm z-HvSZ1-VG~NA{g43}cZMf9QUEoY=Mw|MDM}1#H&5Y43)nQe&t(?w2mt^m{EWJcZSX zc1$vK-}VEPmjvn?$9f+fmDIke$_|@z4?|@{6Tj0`C|4fjR~442>gv%&JMTeT@JJ5y zr5z|`bH1xBw}0-)yrAlbSL*zJwA8p_@z~_7v>a=tS6yAbV>F`OOCPCfS)!id#kFcnE9Jot%AkfYTquXo!?0`4@nusNk6 zA|moTKO0n99*>+58_?EYueZBx8iA;0DKwdV3MoRca&jsmznXg`8TOIOdnKslw)2F~ z4aePQd#|W@d%p}wD9&rblM%0kqEJwrwDyo4c~agWzoDaLN&i;m1RE0EZlT4>%-U@n zu(o4|!+Nq@GR9|@(e!R0j%09Tx`^MJ}Jv5Groa}txdGucD&w$?Qhg>Gd$c8xY{}MVq{~< zF3-hFKUT$^Cx3jsLS$eZRGms3G7p5D zoMKWwCk2Xbv@sbjO;SnTj%2+o=G!B!sA~P>Hwk=d8?jE6aaN@|RdV4QmD8;TY67emXSZ@NOrQhLf`zvL)iczryzT+g8s?S0H1mMW1t@#Eoeu zz5C}k&eF7@Pvk3QEAJ||TKOxf_*>;SXwQeD3*oa)E6UQGe4s$zps%j@F~E4MGdI|8 z*xsBALx%*PoP2y2l1EZiXjS0`GKv?nJzP9x^)9MyOGG2(LX}?gL?aqJP46zS8TJR_ z-&LxE#59jKYx3OgdpN(_=ZDur5rhFsmD9W<@CMFj$!BZfT*dE5FiA)jin-auAUqWV z_27tNDS53&j|fjt<>+dej4~*ldabKoy@v#Zka*#jtzaT_WRlCmF?WT zAwJP0LW!AsdmZ4gQ@A7}55S;^fY(;^P^C;opb{fC3K+cC!rk8TVm(-5hf~rRpX2RV zUgg>!b~sL$WzD{yR9<;13<^@K3jYx~OwL1kZofvy+C64Gh_m zKET1n?JLD&_A^3*L{=Ermku26_Pl%v55M?r@;tSBz5rzz)H63Dl~&t@v{I03wbypI zxFhI1t~#&D`A**aj_spBsy@#+7?u(0Fr=zWiU`y!oq5@zPSRCzBT&w>za3 zFN|?=E3{n6!Td>K|6TTjMN1z>5ox9bdu2PNKO-k&Y{wG$|>UyO3U>gBIoRIMn4UH&T#ua)O)1xBskM zWU7y&Z)BkDZDJvJoPa<+;FijfGppfNkD$}c8>pyWq?wm9+^<>GJD%e&WNXkaPH*I{ zl{bR?Vp^}SSN{oKq4T~}-g6G+1yZC!!Ds)cYm|tagK)o1m}^Ig(*j7FRYM#xYTc9};ujZ)1i%z0Hf` zD^0h0z?G(AVTpeA^uYhYo=VELh!HvPZaT}W3G3!yI|KA}+F?-Dc_g8aAQk+1om^p|XelW2Y2%%|PMW)8(9_@11cj8LxCsxjCej)NK%@eU%)dt6ph z;2M^Qk57JFpI>c?vn*o%K5zI9Rp_ZoWG=_uye)|q-_E958Ls(y$jzz@P>zCh0iY&0H&pNmV0wf*zGDp#i#ZG>!9zuknLyNXYbx57RlqFQz7 z1L*oZu|JLMdl%5*pZ8JiiSYU%zZlAmX3UqNO+2i8l#>tJJyW^CGFsV=G-;8*<&P`H z-)Ts+z;(rw2i0_xFm91h5q;zCg5viHbd-~2-zxfqo`_3rq2L&%zv8@H?M)Y{Z4H;! zHvu@eFenass<7N$fb|jVVN_cFy&I2=Y$>Q7fcfjBGt_O|5bVC4PhnRM($~VzRQMS2 zlm(P>{5IqAWi1WpsLl!A8Ze~_ZL<3EC5-PwMQUkdQ#3seIZ{c>s8Ew8U7y)`X5)x9 z>l5*Ngmqj1V9=41lY^TrE%AvOvEs?i_P!#%L}tTyO%k)*y;YUOZ?l763zWsc6k0_E zmtLtlG8Xk7U;VM-`JDTk%rH_Gr4)4FY)@j%n9(uNbpZ^W4Le5UVB5_{#o3I57=k-y zYr1^n{Ps05>KKmU-_yneHc1)8r9RucS3a7j`@FkL>k@A!N!lD#2j>2~c_wzAt^tsg zsI)Tu?cGZso`jYV6eQrZ%2{)MKOHdLuq74!Ukbh5Q;W$B=`+CkPR(ZT-2vkz2wT`X z_bK0ZV~pH(M=R5JvZgnx=PX=w@I=uDkh0L*l);S*K~M&HT%nf{M;X3hWVMfzH*FK3 zRYS@d5(SW!w0McHn^#xytd(=~-xU?M*A@CsZzCha2APvf5tGD=v{BfSUMB(4q2&2Q zDa>TF_M59}ok+Wssv=SKeNlfRPAV<6N>z7^QZRWEWgFYDZYD+sB-7HI0CJNWZd*$$ z6p5{3KBkxa4clLdBMP-J*x)DIYD#^aN-TBG=M@!ZQ~*&=)fciAh*Nbqw9vr&U$v^S z{&jhz=Qbm9F(Z)LD&7?QhvbB4)+|~G(9^Zvs~KNhmxiyikGwd5?8H*y(`^$-+Sk7s z+1RvkHOykD{}Iz~aHD#|4S&@If8FWlC!SDKp3rRmP}N$M;1^ptmFwZvnzmV6X*6&w4Dtzvl!Im&+Y{H?qodf zl@t-zk&SIo-HZ|yyQq9Je7J@IWZ*IIKX83uZG_?2shzM9z8MfY;RsmoLs7?o+~X10 zZ{AjmeRwC$8?I)DBEG}Ylii4kiyN%GGY}CuvemuC_bn3?D&o-|s-WhY7y<^_`qISGbgEw_L!@3OGu9Je z<8q;8l*(FiWWO@M#)f76oE_X(k?(D?e5ItOEDPdNXrJM0A=L7^0mt# zo~q2{Y7&l>ldk?vs;cvyFX=P~D^JFGFyXDcXjW3%$hvft?!}VluI5jwB*UIQ=1N)t zfu?b;tcjG~M5Q#sz`HHf)ods1 z2@Vd)$PChxx)?|+P=^G5JzoW)|&tD-h3mW zXrVZu@rQ?7Sw+?@6|r$4Mp^CD3iSTHf*ODaipMDzA;yuz!z)C5J?A2c6XQ=2TfCz3 zzmGm$xG4p`dIm{A5lMG)3TF@?nOtg_eJOUTiKmX-$+& zN%^^8++jxq;I7fFkLL&!@m4G`G_n*_eWio>8GR*NSj3lDg42Vk zC3=D(WeB9HnZF`bk6Z4M!4wyiFGj*-Iwgqhyg^mJ7}Kw*Q429mD0K4}BlmwwDu-uG zzRS}&i65@i2T--lHhSjyB*=eP48AY#w#unHc-YodX=w#cqv-rk zFUCcgZROVR8yuuMeeb6XPhjvD`s_l?Q3inz2Ka1xJtLK!=v^Y7i{jSuZ{or2iL64N z=QHP9Ks{9xumoE6-aw=*K_>PS{1?7>?K4cJ*Z(BqA3l9^obI^0xw{Ii^kQ;_a{D+n+(rmg7#0-0=y@8L4fZd9txy z^}EGLKdLtdyxm7s;yT3qJFI`7b!b;^rt zU8Ajx5#u_<<-J7zJ>rLHmWER+G!tduR}(v$#6A zG=3GkgWG15FwDp!1zIy%#W6fQ%p^AYqUropSjvjW8g&7oqFUSW4-Lb%s#n)n4<6aF zNqibJsEaUe_ucSl{?q74akJuz@ls;ttknMUJbYE^d9~8%NCAWO{kt*Qdv%aI><52o zCp03yXsB^ON`*m#rh_RE!<{aFVRh(rq#HQ2#sFjipX*~KE30Ct==|WI&NHdWmL;S{ zIMby@Iim;HDYz@w+RfF#nUvXbXJ zG#@n(kCa^t-$O^(b;4p7jt26qlgG9%mUaBeuu6?PZT6o5Xm!c=ay3cEWK7}Zk)cc* ze2EO+zq&112slpRwq*Z(FrL{lF);#{?P|jW)6NX?EjO-@%-^g{dX0ci+!vvi=e0A& zf9JEOUc7s%Ze&3ExtpTNh!6SbL0*F*r(8~)DrxEmwTlWt4cCZL==$n-_nKer3o0i* zO$i*1l_f7WE|HR*6N}z3`R`FO+)6{ap<9#lz5AQ$U24HZ|Ky4X3ywR2X@CESoeHX6 zRP6_ap9$E{;%W)Q7^~ctFH-% zk5yQigZiWJ#igAlGI>#g0`MA9lE*B<;z;FzNRS1D=(!qVVK+t?$Bt`a7)d5_l$Ghd zXhRVXZoM$>gs?eA&ld6N-QPv*I4z!Lj@ndwMwCNuoSR75SU_YBc)=2%-c*H6iMGIH zNXG15YTsXFgbrC3@HBoi)mu3`D%6=jv~1CdD(gG0i6eAvF4<@K6(#C3O>l+1p8hvk z{xy`Hmur3?&L{$z?qX*wuP#5oo1f9-*OOAJz<0oni?XwW$QBxP*gE%9ZTDG=rVqS< z!ollo&a(L>(^vQOZYOk2T}I9Xj7I$-?0{i z3#X&V+Tqd$cO&dEzNSXQ&afqvI_`5s0*oO%!z}<~Q0Gc~J(&7gm!Q#*~mT z*Wa|b9qBhOXH!WAJU8c><5|^nS)G2Etuz<}q7esijC%(?Oypv#dpBdTm#_z}R(JNk zIB(E?S3U-PgAvkK@g=0FBgGO0m1Rh8Xn2}4!4?iCrbK}J+aed0A6P|_S?OU0khKOO zuQhg)nt%m$d~o{;dw9?k4+fb=tBz?t_vq&6p_#~ zNCy375gHEt^tUVPvBbq)^jakLXw9hkp&{`8C{$LK>**mr{cXLoqYU=m+YEt%w@tH0 ztCJ;m5)wUUy6-2gku9{sQP7m$4qAQfvXlF?m|V-Byf88%7_iA9_MANQbz{(q!iJg& z3`efPk)a`kgT0^S>Lc$2G`ARgm*NRO%``MM!$#J6J~H=MH-6y%;?%+trzacbWeX)X zR2Gu2hTwuw`(y`eouSGO$sCa!3XGtcm^tDYy3@l793fA~Rg4?4@fZjwF2Mfmxup!i3Yc2Fc^XOrh9E`p94|K?VTUiq zd;Y4hupGN%mvZ}E5zqcrrhUhZregzFp)K%Bf=B6_^J}SX&b@elsx~uIOyZYz_1kLT_zRF=O&BNK_^1+T>#|D-jDo0J)2&Emf^GCx;C%Lmkfbx%!g}w z{^RVSTTJ~b3#+)LYIU-bTb7>(e#lGPBfDs7D@^N%WOE*F9wcaKt;>e%W9>q-={xMt zGR!iE=}atZjs7^^0b^8l==?Hyuj1#V9Ms-$QsF;8ZMG}=Kd)5W2XxFDa05upuic7n z#YYc8owiG1H3tk(dj1Dm?b_+~46Kla*zr$2#8%W6yR@kI%tf9@76X>toFtchuMHpv z?yuo>Ia0>FcrbJdWgwltO#UqmMZQU~!oIyoxu&Lsf7o72t8o&?BqS|x?z(T{CQWmW z_Laa1`9G=uRtQ!=qSD8bE-Hc^X?W!bXNgjD%8q^$Wij%_ow4cy`*0+qk}hV+LY<+n zIBbs@(uYT6XAT4tT@kd@ez7xA(Nn`Vj(iam4x4DOR4YcIX91H9ta6BosY-n=WDsTg zvCc(On){(zw@S#cO~g`=`Q?Erwdg7~m9_}^3uZ2!`V~S?7&?R9t`mMI#;Tx@&d=O3 z(#v04ZXBM^ev9NWTxl7qwwP>VA<9MS`i6=(R?KP^KbOfiI9emS3U=fy=uKM(zT;T0 z)&mAoL0C7paTYY#f1P({xKkDHd5(#7_O>H{$cBy;fKg6Sm>7L3zu&tklLAx#EG**4 z!XOf~_&Gzt@Wq)Yq4-Qvg>PzcrHp12ps=M4@!pXBPXqZn97JdU{*6k z`jylL+_nKt9% z{dCe>VU)0^4~`;DAe$(3Rrlpc-6bT5ncmB7TU6H5BWj|0SNYxK69Wg!05n{OVNc8J z`!L8kn0Lib(%6`MCH{qEwG?!!?d zE*}2*$N{*_{h?V<(9XCs(=;4oI?vP52ZJzZ&Mqvnc^)7sOto98XjkfrzxzN*GY~@~ zK$|~nf3#c;mj{visoHE_9v2R;S|A)Ba2`i0s00 zELZkq*=-L32ha+t-I)#e9cT-Ugt)@c>8P2ITbIUr=4zYWF0FtF@k;l8E2*!}R<<(W z3Fc3;zfe(a4I%GGi3<0!BY&%1=WU?k+jc(?@VMo8TT)`kN8-S$0{a!y%k=SalFtBXiJrKD=4t{&gc|Mh{3T#R z!qH6c*88K|*CU5-IyyG2%hadU-_#{@B-(}&UM7GHaS4(zt@x&?e)k3Plhg9@{XkD8 zG5!5kxHLYQ7A3q^`C=f_D0z$?^|J-4GTfGjkVQza4F|T_6wWeR_un~v1CE+}7V-7E z?07(mil7vx4(`9$xfuIFC;V_e0^@gmHaP0{;(w1yD)%u!)K&B@R5sJ^$GA~zP3@=Q zJ76B+fE6`a42ta=&*iT}cJ!iDpDR0VgZnnkjQyV6FMUJ|W-Be1%k`2+E!NjJ1l?9) zDB7;@U!E^DsBKdGE_)l7d(I~%Y3UiVYRi`#b}%#wotT)T5`JrT+^?g2ny=p|2z+Us z#TavK$JZhQd3{UQC^~IgI{ee}bN7#K%jItO0nlcR5U|tJ@&0s7WLay6n(X|%Sz%xk z&1~kjgmpSn;s;o&18LKR5I{7j=q*eT5decpX=+wi;*X^bE~apM->tmJ6hSY>rV855dr+jLWpO^#pWlt97&?#fK3*@^(r~ie9&#&N zT5h<3tXFd2Oh!|B5gpt6eQPb@oL0jryf!m^k&saHjUKL9?jK!YVd3PD7m0!ZvnYJL zhXR~=IeDe6gZ?Nz0Za7Cz)Q7?X)EEU-8{RMwv^vsqanc2QYieoxTlMz+jZD^lr~ZZ zDJ%zPD`-8BM^gbH>SBmtb7y-h@E0avKI7iP_F8g^zEkVebBWFI5vw1BL1fiyIh&h! zRS$B4k-51|OLm}yK}>%b1Txavy|*mYYU|gk`rA^K0@ZV5ui+8ao#x+g?KabjeE&1< z@8|F3{$sTT2`H*v02MlKI0l52ma+itJpc0*dn7V45<1#e;KerQr$gQ2z(Y=$j${!M zUvEJ*2t@cQE|A0fP#4^}g)b9LqH}OvB=*1psjhB2e|`Y+3c97m`6QWVmD4yW{gM-$ zX1dGcwCeWh6X>0YR(GZZ9Dz>Ec91 zj81?y)SiU@{L(be`F2$tRG5FdAH_Ze_V+aoI1vFzr9ej8d?EMBt~oZ~x659%*f;`) zD8Q4jAsbue)eReojPj=Bx&=grKJU5PwVCEvXy5}%Ep$G}muGp|XKuOK?=P&kovU`Y z-LgeJ1-|8bR+?`^+w@%ahIJIIWY6vr4Vpo7#th;Frrn#rg`vhY5?2~z@2XDLseC}jsU)c<5jqfi7i5*eFD zhjOF($lx|%4{gIrre6~b0z8*?BOmwF0TvD%(2AbR$J>awYm+EV|BafB%l@9`DX~LC zZtoRLC&n{=cAIk$@1Yg`QdffYSpc>JHqceokkd1|t{RbO0^ z!l|s!P_LWb5V@hHv^IX4B>9Ryi$$#?i;mYmR+l1^H= zL*acc-;vB6t~HWaeL2Do*>e+qZtR3OPG-A0ELha8d9=Pk1MpmOVshgpkU0nlMwiks%T=!`G z8_v_96&q_}FGN8xwzJ>9B}f_i`DH+__kAh$g~qd%L%x9;(sa&5L$%*nXlViTKmbV5 zXlCiMsd&`W*@{xov)-FKpIP4c#uO(6_{ic2d+mp7J5iq-qvwnJgUiE(Br%bfHmEyS zP)!XNc_!DFavTP$tZ(n6%xK?jeUl__Y#rb%)>7u@yY!kb^~h-9Hd965(;&rhJW$r2 zm49~gCS38ajTk?kPMAN`MO|g|%t=-v)Z&kXCClyIy-({@PfwE^+~nbj4waJnz(uq> zTk9U){u(N6wT(zIX}S9D8Srx$JLAS!%PgN`zNE}jPvQV`IIrq&1fZl@Fkfpruhmg}&&f=E>dFE@95 zSjP)A^obRKRXm25=bK>+GoY)O-jYzm4Gs?Z8Wm?L-f!J;+2OCI`I8{a?+3S_;8h3x zKgM0u{ehBEAQ^x|T=#Y%s%PZoh7Hj`%8-{=f?w@=e!3*{^4MMg92MC318lb%_Z|hd zZCONpp}0OQ@rzq>;19!t(9DXWrKN&a0D|$|3)$apxWVPu?BC$zOL%E5{>{pz& z$7Nqyw?m|^_Qpl^IY_=j9QohSo3YUFeR=25J*l)&mgmZAK6uSjq<@+00a60$$)#~# zAbZ@u_Ei=3bUvN#{5%-bUjCXCJu<_i2fGz=P}u71EBrc43JbGz{oHC5BB!j>^MUK` z{@XrX_e>bs?HZz@mOevd?qsc_NpNtmK)@|WI67&UMeUhP{}nNls9R+g$LHgAALoga zz_^jUzBMPn=wAmZ7xmgzSGnw|gFKJk59;f4(G0|zuPrtMbw&JQGk4VX7>yWz(#JfR z`ddy63huC-mq$xczXyAnXtI&BJlT}%QSQg{mV>6daUt7slNDPCravOORMGU==ti9D zG@0kB3n=CJu=e$r(9_Ds6$HwS9}3C*w)jiFR}z)bzm+VY=m(FZ^+f%BDY)fuS2AD| z#D?DKoaJ8ufNIrbJY(d%KgxUS>E>(X*Bqd(V;8w0?R=Te;z%27d)O~aX2tj6sbW|L z3`g(sZiluY|J&~poWJFk%fiHkfWN3KJ@@mIBRSyMK9~E_0MrTtIhvpOlUSc?-srsq z0x}9SQ2lJ4ZTuAB3{6mBqU&WeMY8n&5sAx7#YA1Fz*$jIN(FqoSGp#@H5%j+3U{Ua>wcKROJptq;GNF zfcbWdj16inM?YLg6B$p88B_fPbgNlWUlltOD zay;tXTrZz_9%G+tJs@4%Y#m(Pt~_fuk9Sc?{++Q9_I6yDc4L*sOSkL8MS1<38jNq*wwH8_yN^78jNg2-7QXi_8L7*ay}hp%SgkR6xs`>PuXmidsB8=7 zW&CyEepMQ;taEpffJjaWgahdF83xQ$i+i4dkI&WtxAPP&(7uj|J>NrzYZ{kaRkohX z_U9@oB*QUwHau6hfJXt41_A$M+6kH3b9=O(=b0_n2Run=SIi0%*xR>x#snQDb`iGE zSk9)YH^7!fw_K}fWn^L+IJ(aE7M9At99VNw*W8wHC1PY}={MubvCb|9scN+p6ale} zxQb!r;(knVn9zu8>$4aFaTyPe$v;@?j@bL2j-}SGr3JRNRwBqK$W;K;4P`F{SpPj% zTpvrf>OKQ&8yjc8n~c?d7Fs9Z65!yHE|y<86uXj{m_jQQtmWm+qv`}_ZbY48TWkU9 z=*N#4WJ>AEJ98!M#Cn!Ry`Ov6D659As!A8x2L&1sO^gSKAsp=P$62OgWyW(Oav`&23RIvcZRc@6BuP5JkNw~9q0)zj z6!cf{<0_q(S9g2Fag{uqy)~Q%vjt#`lR}`z&^1}YlP+oW%_2scX+kLsRv= zni`gJE6!LR7NJ?c86OrtR`i_ujW0CyFNmtoE3M{r^>lORapwY5z}SR@ayQHEfudI| z`(_;b5fSM0)bpMulm@{7hbDJsdhg18`IL=KlifO%+<0;d=&5xE)pM8Jq3GXBq}LpZS{GVtQS^% z!-v2>BTdz&1~&h?80`RB_ov}X9!^#)qz4BtuHY*zuwzzBL#=57`tvUY3@s{j5^mF65?y8}v>hzOg?V}dfJdcXx0o>f&c10$CBEgR^yH09r= z0V7Y8NB5T{RcGT813+V;CjVf$YdZf>r*857;U)!0|3%IIH=KenppibIBCBW$vt2m* z{~f$f3FG3$pygtFRQD)l9}@Fq$qs8ea_2* zJ+<0r`A6>+X<@;IB5$(SJG7PY0Ir$U49`jU(m&Jnziwv!ugpr5=kPx#j_FjzLTQhw!?`xq#BX1L@FUD&#sEc*q6(Ffg zzbg&K{zk9#R;J}Q%wF0*zr0W*OZg5~j_cgN(^$>J!*V;t(sQSEf_q|A&U&taHS47u z&A?zfo8UvD=VvJ?DdoObRpxVx6tDK(ca4^}u&}TMsnEtTv9J`>oW4dx6fibPN%g7Y z8d6h#dW+14g;k9UpA-?H8yT!1CB-7$O-D_gn7fMo?_MroQ@6L!XA$baauOU75s?tq zE8~PsPe2-AwVCuE;vq=q0AHehwnzBi_`UzLqxv`Qo-e^2DTey?ud=yNLsDYT*+L^C ze#s{j%CH9oKX$$Ut8`7TR_$>U-AhMzyvYCYnfcck?7Hs%kwnf~P~uNzJy8w`3Wb>Q z_`l3#Fq2}(0rvToYNe#(Kk`|D2L3PY;os2Q|D19De_{Tcy!?NsBApXFUrCMQauar; zhEd}7@JN10-7?BQHzyvOi0U)E|mIlB74PG~k+Rjq!85m9flQf}V-^puxUj?y>$q*N&XYo{L- zVWgEd(e(Z)RD#M5jKmRr&K4_|QqoVChc!7yBF^R^R=IkMEtZG&iqydiLW3 zue=-4CV*V5M}7SogC^E#-HU)9aKRdmPL?k}ZDHtrMVYQ_S?!)&IFx^8qN=*OmOEQv z?Y#DdfhC>@X_u6}8rNUHAEa123ddEbnEY-eiVd{U%^NsQD(Z9MAKXppzCX*VNC9&3tBnZc|<@P;Y9#rNB+=#%KOeDunZ>>M6$eiDQqpGuBiLFyg`!zQ7rgCGo zJ0$w)k{jf<|2jx*m;wJg&e0{kR#X7jdUel!JRPoV)bdTU znG43-yBVf$a1ieFpwI-p6YS{-<6|-(_a-ZFyt4CA0x%V?-T%YdS4KtoMeh<4KT^6u zKtkzm5D}18Qjn1D?gr^bKsuzQQ@W&)lI|9U9$@I2d-&gVzumR&m-~SQybLq%yX)+| z&$FLXGk_2t9sTTu08`_bh2r5w4kqmF07*eo^C0 zDtS5=`>X~n=c%*rK~K5jdwyfU`)WRsLlqolo>+mmVOCvNWl})lD$iYoaIJ zyt8nMhoz5%T{*FW`js-CW1;?+wLy?I#sAP96_iuxb6D%V=uLKF`SA$6a_Q`#O7v=4?WJaEueE)rl8vtJ zb#u$bevRXU*X7}&Qj8AYLanE>hu1eDo=&rWXf%_RQfSovJ9`f6_m@ldCg!GzS>H#o zwXk1)A`O^gRH5}?MDg}3%0k94>vZVr~v$s-3qd>xe`opgW zmDgfAJUc~qq@0Y$<1R*^>A0Wb#-T#QZOBcUBO zIo!5BJ`kM?=AzAulIWxr6TbvPf>%O*e~I_?zQQMWQk7VI|WoNlhT zT=1QD8X+`3ZgUj3MSo!K?GW=W(XZ==`X3H)R?A9LwX-}IvaN%RY^XG@>g?R$CzhS9 z4?{y}yfHDZPqOA<4Qv@S$68uZ3gvyA)y;G`Ye5HsK=6FlGs4F(SsWUPL<|g!lRKXc z=by^oN6qqOD$>o%b(wTiSl;F`B6g!h&R!$UhrdG;g>VIVV4s($En_v|#kr+OU%pttbydW{9>F z%BssW*w|{gQBppo8#WrpMRuQ~p`+YmRBv%%+2ELm92yb5VnIo{ME~0WxJ93=rBYRX*i{<^Dj<0i0A)ZIBcA34`St5n^23d><;=z z4MORB^(g%A?FyAcNABU}!t4Z_?=MXVN9{vXGZg*JGP{s;FH0W&NZa$nZNi zKOM9cbIQ)|B-t2{uEO-siH27U2D0_nQbe0W66uin1IaUzV$HA&{@ zFPP?(26-KWzrIV$J!4M@Px?B_i`%%R={FBty;# zUk`XqA=_J=LECy*ctXQAX5sOc_{_}A(7a4%LfvB8_0uo7D41b?BL|!N$*|&&PN_RE zVl(+LDyQPr)_aO3g6JTu$OPWZ1)6i(MeD-Sk~z3NSXrvp*5j+~iaA_ChS2tbt0>}c z8QyNULjrb-y%z#2y_RoLd!X@Muk-jOgml)u0rv<6M?|b~93P z*VbvZ_l(Z0%~-yy;MdBb%1Z6E@Fm|KZ?kdAKg;1On0Nd_k& z#%WxNz7hC*QHM3(ku0%{UuBwHhW8#`m%G2?bEVR_oOhr)JI)`yyo9H~5DgDY+5EY5 z6mgBs&6yL+T$)?k5o=V3?`^f)%xht{T?d(AH+E(Wb|H~-*g9c|B7rQ~SL>Tdq9++< zfbk)3Xfu;&yJaF{+wC8gWuIX-Q85orFc{8ANB4C|ttdV<_5JsMCLP+<0ai1mj#JnI zgr9e!(|EX)5?N5O&{ZhJ+*oJIH1e+)QYhKYnR^FS#B*n)NkTZ(2(Us@`Mmkd=E`Xc ztu&s7_-`)uLsAUW8Lek4O}GDiH!(GxXmPa`uD4gQu{n%5C~(ljrr@1c#mg`jQ|J|d-J~pBdAc9edj~}p^!5vufq$lR&EGa>6H;<60`S0ucoT( z4aoUzJi5n!=SqcrYOx`+o~y{$tk9{qUP>-fN)OKA(6O5;QcB~uCD&e9aIsj+9?KHO zBP09o`RWd!07~a5Rx%!*t*EzH&8!58UuLH6{%v8k?D|p&=0dx-fUDKq&ZOVNdpWsR zmt?vPK0?FE9K}wm_*5b;KeX%2n3ntV7dRcgeP0qL8~7PuRH~`F3;@ znd;gWTAk|>e>%Tq_b!()`m><48NNYAb4j__#@!H z1THgRUDc^9FE20H)L^sF4XoM|a?~jI{yoBaCxGlU94(b-*D+dJTKe!BJu{Y#D|)BHz(Nquu=x6++k=QT1pCfKbGH}dt97u4AAfZ22?pYIa+S+ zgl{=M-9G}6Jx@bO=4)M9Bp&X0z<^n$>0K<eY(0^(w>C;aD+tjwK3qDCc z&Ti%B$Nc_{+HDBzeEs+R-cE+X^6CR910IwFzJ2>92^6)^Fnf!M#E|D`*&bms@9qj)A%hC1hBN})prx_*2 zz(cn**;vS{>{bIgmnpSU6G&H$jEsId^;Y%K&r})E1SrX3WzN?aqtUd%#bF@N8e+;8 zGDmPNtQyvdXpuptCx|t^SjGPZ)%NfIzCTQZG(qZJ*DY z^kKnVF`F~6Z_Qt2P2881+l3ie=|DxCMw!-=0W}|tiKI!G6a%#k3Ko99 zQG70Dv<7@1)W$`F<_=7E&i52a1!A9`68wvY*U1;RqeUV=JslnLt2J*9AbG>tXfn}< zX|;l)B5upGe@4S)AKQhaI^o$v1dJy)sa#g;vrZ47= z#0k%^|52Ucu&8C=*teZRfQRR}e1nQjIVeq{j~)PkkyT(w_xbAYBy}1V|9iXu5xY*R zs=27SgY8n5s%Kx6$YlFxetrzXIa=Z=s~*PwGPc~D>Ava-g7AInoOipvX1VTAMdLF5 zv6FMNZrf!JoD=Al((TT2ZhOLx`LMr!>FE4mi6nrCMT`3CLJNyDVCe`4{$rud6jQ|Q z5Pj~WC2!|Cl1`%?c8D0V@9nuEi12;l#nNs1&<*0TibmC9w<8B_`0czg!iFa88z76u zRVRh((@~HVJqe~#N5dv94t`NJf8>Us!)re_6NDP#{~@W!kDiWhaB%SHhMQLGN;ONK zloNXJWn#0h-ew^IJkZvK*XiBE{mty>+>-$_%ROa1^OUa#gsDZ^JH%Rv{V^cE8GE4L zOt>A+BfbGas(VWLvP=-fR1igK!2~1d_mo7DO#M%EE8( zi~jZsyXOa3-kt@8RXx@?PBDU{g9xXA?&4t1e6CXOU-JqRtD1`oZxAYu$!#=QSe!(; zcF@w%&RMrz-jnkk$fO-ReF+1O@hwkuEgDnX1*Uy=Zg}n)UO~X44s~c+%H)DI50NGa{v2s7_R~X7HCRm z<9_gs(7MFA7xLr%!|eq?#!(7-RI6WjueD!If{#O$xT2(Lb=hcGvNs177xG!QOtZoW za60yhO9&9UVZ*(8&7mgKv%;+|(Ff{Do5b6)*0begifP?DqZv_DtQ@s=E5ZomDJRPj zF3`pAv{GTU$Hh4gtt(Ha9v+S^6GPThq?7?Jxa`l@Ob;t4E3?<|#V2NAzj1kGSK!_k zfT;LzroeBvJlJO6kqe}+J72r~A7n__iZ^p$3-O1O>ZBt?s7jgY$!>!kw<49U(2#m* zCc0TG^xM4hUkUMvIzG(<$d%itQh{9udIKOlRII_pAqwvh^WZM7@bV1 z@c8y)Rdyb?7`vUD%TIWJ*K6{XjvP~CE#<+8+oEEl=SeKpC?(9;IrRt^qN|SDXBTI$ zRKK90Am{-fJm2QY!vgT-f2K=B@9tqHCT3?T67ZW7+^_SWLOUMZYD}+@5le~uQ004A z@9~(;!$4Nu#VTr_&$>uRZR~ZsU>>*d@d1FsKTCx=ta?JIV3=1X!Y;eOy>=2+B(UR( zUhlv4fd}D+M?~a;^uADk6=gIXEG^n~ZnhpeAbnqB(;=kpbK|zvX=G$z2mwn8xAmn> zCAHv5fx$ks1Ku%Tbv@k|g(#18LpO@qdSV0Ga~b^DaKNdUXy8(*v*D#6JS1}Plgh?M z5lc(Y5e4vl@qv^vU+V6113^S*=C#Iqd3mGNHqSnt32=SOUPV;W%*?E0pBt>TU|Awr z`ex58MWQk9*ih#We*|9I@AD`GqJrS@X$wy-aF>@13^iXix{X|GnfZtU%w(fk?>!%G zjRx8-4;OksX=1ThZ&_pN*J<(bdd;sh2W8Dy7NkR94~|*mfnBd58Wb|*ycS)6wQ>Bq z##j#Ajke+X#qKIq=bB2Euxoct&Z6tS!|q>Nq{qp%yg}YWjLJGtB)(SOcyS0a{L>tN zcm{ZnM<+;DG_^?b^tAd@R?`Y?PXP(jDg}S}2+bqxUowZ=%bGl>+h9IOck-GjuU$n1dq6WDFJU*AFf`o(G|0JPA~D06dfIX z(iKJ^r=YNP%9LXQaw#8iJIZnR7R|H-+y_(^{>z^19oI%r@3#Ud{t8~Hz1M3?HP7+S zzN7Yuw+#tWOD8%;mv}hj*%<|$NqpOtQX&cgI}?yx+AOs2wT*xxXB~2JkOnFlMiC>3Ud8#J9DPMmOcnel zu;x{!e&JKFWow?b_{-9DLb~nZatW+K!EyAD`p4}M>Zer@#?jXGH3ajof;$az0=4R0lm?_7W5n7CHi2M#)zaWjTJA zWOKUyVroUorVpXm(Kq+b+}zw2Yn4<8{;w&Cyx>x}T;5}p9eULPcQhUWVF20nruv*@ z9ES{bg5C8?7F+3F-R{}r!>L!hVz%)2x$?iM-v0*+pzkjx+75KJ{NN8K5&wbx#a&RI zN)VKaJ5m1;L}Kgi&i-P-?7WL*m(3I)!s*NJp787S!Y9DLwzzh9yvTu{fUHHzunaPz zrT-rd#VZ}<3Aio)64$`-Q`rQ?rBNPEW>c7n^f`IF)2_3l^7p-WL=bj89CQYxDpXXy z%eOz1QU}qXo#QNk+BAxz{aMJsS5yg@Vw35&q@>=bB4mJGrrKy*UKQL#aPTu!Z1Ud8 z?P1S{7=iblNBq?A0|P`0ttP3h@Ib}^=Oezk^;gX6;MPHv>gJK1w*e_E85VE%c3xs@ ztc({i%+Gv1Jz6SyW!BI|8k=3!79Q5FyAq_;^oO`tk zW{sWT2y<}6tv=jNRN1c?K_s#i%`}M}Yib63IaD-LKzTnhq6iAPKhtm7Ern$5AwYGU zCc7UxvcG8pSP&Gydvvm`W)ze4yhH_ zx||xl&3zum&HcVo_ogY(v-bAl?&06RfAL94F3YAMK7x$n^z_tpaV*|&y-`H2H0eXp z&Eu{aAbrWfkhMsbHgeLS_PJ?4F}9aPr0IZptu~m#dhG4`mW_3~_G0P4yuC=_8^H|F za2Nwv^O3PJv)N3cXfl!Hq@*NnxQEcfj8H7tXd$|j_UCO~p{54(T=ecIz+b=Y%o%Kf zvP9ukt8^4)`X8`_1&x=u2M#l^7GaJ=bK?ZmiHTnWn) zp)6&P5&FuwEfybu+Gc2Ucq^-MO?(@3M)PHvR+ZV=bO{!@V7&R`_d=qrM?nLlS$LMoBi zoRl%h_}ko=BEBh8GLd1w>dcW)bTBn#Ws>mcKta*|*Me(UC;Oa8JFTgS5TAhKCEe!r zTpIzN5LxG_VWWS)rGACr5c=nZ_(bX)p4z}`)=%P?tvm^qbu!%kdoNc#%ox9M znYNO9e*@NDm(t$aB^dHK?@PKVb+h{HMW$-=*qPIKZ>_R98S2if7)G{X;qh~8mCN)* zyD3L;?}5*Da#L~HZiidXiws#VoT47}4&js?{YDdd6p!wNi_C8O^1FiGmG*NuXoqKc<%+-om2>npsTpQ6 ztKN7<&l&?gM~-~N&^0nGNI=)4V;O{nLrdl4di(mHQ&348*~7cTs7DcgB9!oY#w3S6DtBv*w!w+@@rPsw?j_U!^g)Kt zDv|K;UP*>vvv-ciiqwA--l=%jpha(j`wcQy)Rj1p3H5(bVnwCuoY}+J{bp^H zm@J%Y{LC5MS|!IrU(L5scj&Zd2@G%;%{AE0>$&COC(0=+7O46VvD@loVS4LJtNV%1 z;drGlI*y*8(T6)V2|DbO%-Y_6Jv?05ZodHnrX=ETLXu=s+SeO};VS33T^&C=(4-wd zeqkY_%22`$ch~c`DGC}Vv0a}#x5>j-{i)v0f0Qx1{f!-R8uyu_$a1NnmvNSnu7>ej zrA0#7v=KCR?b>umw~b$<`9@s0zJ<_RQM{uK>GBvJ5nWzkVg_Y{j!GPWECb94vp|i) z!!y_7&$$v>C8+IY9bI;@#I@d@t)wjMvRz ze8n|g=}67rN#X6nD&s=;j_HfD;1J9sJTfxR>4EY7enqje>45A|S!Bg~HAbufW2?Wd zJGe@mR{RE`hAV=JZF7Hn_{I{Xq^;Y;d3pJW=%`51WL#f~D|9AWeDBm&@9oXTcOC@R z{z>$NPW*7(BIw{JMdx(hSLeS!C%_2{dU~OIR{*zxyX|BuS2X;P=TXcAUFmcE3sNkj zbjH|j7Ne9x#eq8OVPgIYZK*3nWCx-LdZWF{7AA-2@qMT5>PR|+ z!ikqCuk}=@ne7r%wbcY2_vBPmJ3k$Ff?bM%zKzaou9VN&ep&tQU&MgkKi@4~HbQ!} zN}qL2dcwjZW461MvtqTcYHwf>;gOqp?0U+UOVa;FN>YED3RW-pUV{^I*wJXGWP*=Q zF3^ICaEp5+FaEu5@;McJtwHyX^F)~CTgG=uW+sAG4vONQPUckb~{SUI?tlW8;Z!Qo^b zQ*(3kwDn(v048W>j<5G6r`zp0cgmhck!7vJBCo3XDAEt(9PuVPx+iUUY!l%pB>dFq z6i*Drh5T8v$3+{?K3bA1)%iZSYvEQ<$hAKzu;(zu(98Ie^Cmxz`NYFwrGi2~4lQnp zN`DQ!bZBf)kgldOXZePmy%KE7DP35Y&vz%#x)AlLe5M;QGdQ0vqkcq+FJkbPa~IKHTr!NH zXK(#0=He!8*ssq=y#vXQUt1N_)NbrnMZM1V)HY#DjW&kbRu15#Z34ay*bS zR+NoY@p)w8iloDE0A^37?F@tJ?mTz=*IcbI-}huHk;$vu14Q5h<=Snjv!DrC0*asm zZ|DtV&qH007*Hi)H61maY`N!n$8LifV7qbo?RFC3C3LZGe|p*o#&NepfnY>{uiR2W zcXn4+{d?Ob_4?i}PfGdzw@uq45?wyQ;FaWG)XG70?m4sgU*;Sz;)l1fWF`iI1QPst z^jYd_kYcrbbev;d1*2cWlo--m)eDZib_#xhlIryYJ-nPj;-Vc`PMEVN7xhEen_s6Z zH5%|jqzt@c%-BBTlh7F0+=8KY&(k3$#}?gpGBVu0XC_a<#jaxu=P70c(Rc>$rji0y z!89IUVUb3ANOJ2j#yEV<{{gdjO0HvRbfiS>#sq8(9E39;+I?=cB0A8l*!&8L*OMWp zo^j}6bK-ybvOE`28~|bVQh+JU9y0RVt`W4_CEYZMqe$3IuIFpY4H{#?tENi4-Dj)x zq8fh$0He;c?<`ap&n{$Fle3zu8V};=RFw4dtJ(8zurIben4Ld#LtwXo1F0H#+qu0_|t}W#zgGv<#liIYLZAOhI|a0X za4G>ZtQ6W=toaH_=iOK=hKOJf5uS7xjj#{Td!F}Q=1hbqTlFEFZ~6TcBzgmvDOH`Z zl1)5?Qn#3BQ>9$}`5&dcul??*iJ>ReqZFf6Xrd;qupyQce2g>?^sg-VhX_Kcxq8z- z6N8uPa7i3_%eBs`E)wp|=Q+VD0td{H@s`Bi$5R_UK=#q8oPpy-%7uRUGK!Qup>JNi z0ZeLF@FV-DKYSx#0=gPcfOfF(!}$RIVSaCl%w;PEB)xts^(FJL?`7&6r3Y1zzk$eY zC)ZoLON|^)nybgAh-|OaD^`xaJyWP`zA?K8`+O^YB00)wZ_6%^7CPNIP^eXE<|MBF zB~Djc5{brY#oL34RAW-M?oJn18cU+(&NhJFoO* z6f+0)0Cg)0iT+?{j^8F&Du9(U-KR5-X8dp9-oAj7$h5!hu7Jh};XZfZ_FC`R^WA z7jsX4gp1vCUhL07(qyS=AAeSIeBKy+9kaDuf7VDP@~O^=A*a(HKjcNIx@mGak@z;}Z`U1jgem=N;~r|9RH_)F?xZ9eh7IiJP$PezLL? zbT|rIcMz^w&^sI&q~{SKF<0kBb3Pk%giMvVVp6g*1?t_Q8x7Dnd>K37yFn8aOp6{Tt#B~GS5r#qvW5s;t8YUc95n-*FO zkwqpaPdwX4s3o#t8GM*Duv!ty4TqukDv0@Wq25ed+oWC6*b-V`)Up4)V=Xri z+0_u!2U1mzHo#-qxa>;8@$dj`yFZ^&FVXofFH3nKbSxa_rCV&LckH!GEA9yu(+6jp zA!M?X`1|iM(B*j4)(wxt?2Bwj2xl>DPr`t@tMYq`J6fFl9+XA2?U zwYYaG8qq`EzSOArb8)ps!#=d@?ZPa;uNOvLjUs>H^X`u>#7O- zz?s9gG+XC<;1>NM{;Mc%*ri>rYgotOa!1;Lfd@ef=4}TkLx*kD{ZpA@g zktAG=vP7pF*|7ID8QNcbEtvn*a6?II%|F420FQqBiql^C@0fxq+s#v9^Xl%u>xT`3 z?@_5m5h5IM^G!`tIvtkjhf8&6VN-J7OEpyL>iYmE^kE3wk2M9(=UrJD6ykWF-j&;5 zW(wW!&3+yhHTb`_Mn(Q$tZX7xFXWe~l@Wu(AYbs8;^Qd?E!qY3x>BTGZg!U7(+mgt z;NHP}NaqK~TnTDD!yN=2QEgnxG(xCd^TN?hw#FiQv-W7|1c=uLLtZXni9)m>g=PY3 zTP?W$jkXTLS)hSzTtpG-xR|R^pSd4`%r;iPCQ{FQh_7xD@!nG7dL1b|yj=Op>*D$% z|GyDZo`_05t5fL8-3!tmdS<8!rHmUj3xj`#2h4a*{&|;BA8pU#jLJFuBY-@c%7~lG zQZehN;Z-PLU{KQcr0Vgr0SCps?=y~AM|yANP3DUHaXP&*$>5Ua#Zsb5 zeUEfi>93zH{xsRw=!aS4&+$$O4zS|avG6dq12+Kk!SgPeN`dz~y}t_HX&UB8y!mc+ zu`BF{B$HVg{({bkh{#KQV?tH<^52eqCtZ!5QeGx|UXbMGA)_2W(=T5( zzgxtzuvim&W#T5vXQyYQi)*_Jbb&v}y1GIQ=N<11VYo}DRl^M3dq0l0SDJQD(c})N z%i08_!3bdB_E*dj-8D|9_7E z^qn>G;YIO`;y?a4k)ZQ`gGw4;NO{Lpn75vg2t#}E;acm%Nv8wXXpaHE8ZEtQWn@QK zKVB{=*?%}_f`{ua$?pG+Goz#=jX)$QRjJ1#C?qi0zMW~Ei;30_bY~gd^G$qwPKL;? z<(YI6t?D4>`1^&43Dsj~_`TylZyfl-QGAleHJrHcH`$qlr=6g~`&@=ISz;{e$81P5 znB6H>WMdavZg=363S@9%A9AJ`YD#P<{k*|5`+7ujAF)E#7EWfT9tQLZ6 ze`tQ540I6bLdaDl)AYsd`&q^Bp`65{23>hKnAaPv>Td7`Oc<~De39i@*Qoo+)g86i zwy8Q{6p4=t{8H@G)s_{4zW<}CzV~qsQznybkBnXCnb#!0&As06&d}A#{yIr>?iIqS z$A-axDBQaP7k+yOYkN4eMks(Tjce$6!TOl;d&c1nzGx~dFwVKlw&(?eG3J^!hEeyH zG0m0F`eauG<{8Rw#OMC)eBaw1mpC(tnEKpc*;ht`MO&j<$W8K_SBV?f^f`t$e3`4N zIABDI%mcFx;c&U-p#bJ_CU#%zM}Kp?cJ=1E80tHivc=@sV_;W!&?gq_`FQ0+<7T&_ ztm{LG0QS1umPMCkbxSrv1|G*^*_N6vvPCRhBopz6KTrA;AMCbV5qo%EERgKETGF&! z9J}_<8hbW-hE>^ql6=)P#vt8@{VE8g!aBd~dob-gkh&h*@2}7QASzW-I&JW)!+z z39(QYnvTuwT44Nk{_DpC$yb9nez<`FIofK)YK@OK7?t!yM=Y;*iTDV0A%I@Cfe4RG z(nytI6MqviTp=71Sp4{>{xq>?#kj6BQ&QiKeQwoDt;W;bUnmGEE-X+$&wa$HGy9C_ z%bgrx-^u*y(R@C0DEuFboe44dqI1$(*-ZA$4t0S>YlF+~YFg_2{MM|qrVJyv;|&O(HMl=Ksus5* zuL^P|AA2{Oh5kP@$u&oFY`nG$nGIPY@v80Q0QRm(c86klAb<}bs^(DNcR@uldy{3} zC97e>Rn3t^3dyyGE93PxQ<0Sc3qb}~>?STY^hl4;6@ni~JX?ysKjo!F7 zVEsxLKz`v16<_r?M%VMYK!@6h-Q2WYHx@TPj{CoMBugBKE|#Bp{iOe;Mmyu{3}`jD zhbN~3CrnI_Go5TrZr`6xp(5lN<)z);JoEe16fPj{M-s=Bvti%SlMoUG;=obSy!!va z0$7prTJ~nIx$*2j#5F@tWt)yVT7OiBT&I-Ty?}i{jp4BPGnd#&w|2A>B4DsF8HkK< zeZMtMdi#gsNkvW9f{Zcb10*@o8+Bv^;JA5F*YO@!{S&b)FBb+Do+vJw*njDt?f#vT zpuv_dsj&QERKl0Lt!~azP?qrUXmpM$&$8q;5x19>_-@b8u8N(Ph~~a|G6xm0 zW$;)|3nKLUh=tt7P$Ylye=?ehB`D$#%i$eGYMyN1!s6+?q4U>*$$mNH$KATOzOQkL z^Ly7{@A+Qatu}%axtVk?cj4yHj-LFVvuLsY?gAMNa_@1e-V(i%c=!DIJ3K^0yb?Oy z4|siFS1@(oz2o>JEs65309}>v`8WNyP+FG~y7vEq&rRwUZ0;Uvh7y!kuqhHku3(~` z-D8<)LOj|#S!z3!F`+ZwjXM-=+%9BT4|6QwFuBOmZ!_u4Cc9V4!9F2GFoFVxFbh1TxH&j))Nj;QZDLrJkvLpx8|xkTKSV)^VB zS%pQH(zw>{kMewl)4ufNS4BFz=?9W=Y`$!B+ccVGe3o%x7OJ{xp0`GJgV>5dLi z@nrT-y=M)!Hn?wOaVgCfgXN;{HkN$g)K@R_|NCRMoG?aq_1&mVHr!)|L4NYo zFBRN|@|8C*{)%hgCE=YaZTZHI_W5>Kw34A$4Ysk=q9f4^u(VFIR}?tVouM(RRDMHz zbNDSq02dTjHYeA7D4FlR?M#55a#g9U!S?#F2R<*HcF*fc9_MZ`aA=B|#_Nu%Mm$1S z?js0+ix2nJK^DN9)A$J|*qf;sU)F}S30RTBb(`g75V7ayDs>C5q8>gG)W!5=X!7i2 zz#Z>TJ`#_lCfeV^aVSu_XqNI*+ihqmaJ215=-2*qlJm1Me818sf7>pDJ@4fsQJ2DI za1F%yw8;GV7NSjzMF&wifMzJN@d#MHpawS@VvLoI`%&QvhBITG(-FgDSTA?%$ow_ zDwMQ#Gi6RtXDpE8FOUsxxXufzsqgF0Orcr1J=hV1lVi|O6vq!~%_&Cy3p4OdYp`I) z&fsm>hF5Jd*;+PMu7y|0NDU9rINu7|m0@*8vj!gcIiZOm=4+OlZI%9_6uD>G(_t%l z^(u30KTRXoNjkm3W-L_Wz-CXNDkjg$gzqy^9KF}W0ZNkmfmW?_h9-jO)Pc~_t*p+k zXX~0a<+ckv?X6SQa>fYiuZirj?+^k2gJRzJUuP3I?Jc(7w?8wbtI9mx={x^&20kzo z0;xIf?>gcfuY?Vm~-8U<(#X@PHmx1Gp`!*n|wA_{`rpwpQ`uH*p% zVeRI_Qtv-8SNWds1GTepS_qcTb&-599i2J<)XD^o@L>^Ns}D2A`MbLj=27d6hCqmc zA_aKKVq*>BgS?F_&QGKo&=?9;hobDUb|Bw77rs{?t7lx!HdNYvoJHeG9O?yLu{*t@ z6WL*(w84Ig-*n8>M~W!S{4$kYthL5t%8J-tXLQkbNt>u0B_@Z)9&XKf+PI0ZyIe<6 z97_$ko$>!$crf3_7-{Zx<*@$=QF7Y`etA+oYjk&gyO>-e22+;MuTy4g9beG+4pD}W z7Q!HiKQ(zK*_FQU&mah95Fv=)xrw3%1?D~&k^3jC|tG2~{GWg3qMvu2u4f2;&z=ea-;#l1zb(Dmrads$P|Dp3xQm zV#zZ^I(R9$_$8BhR_lS?ru36kW{Zv`|p)n&fa;A zuq*V>KEv>ne}A5{FHrsdhu32lp1B(5&C(jEAg_BxKq4-AwAM}%P<@7nU~zxHyPQw) zk!7yd^avIBogL;E2Ft7DBVf=C;}c76h#&3(g@dJo2?+5=VwFB~L6~i2?AQK#vvP}@ zkeuGXO4S6c6)!@_+ux-O z{sO$?06Cysa1;Gwdu5>10acLtT zBnh7jwvLVt*3~v)mD@ivWjZ3cJ`Yqf`QJcWz1IIqf0kA6cb5PvWXZXv;GFz#+^BUP z>E)uxIr%lxo~4eN%1zC?*cq@Els_1w(6kFDk?51XFA`7moHBE|>1L3LejtQ9A*#oe z_k$>e9)n)&*51MjzYxIRB0wMa>iRRPl}}r%sg$Q#PFw4yL~Z%NOlz3StRdfWx%XQo zu~;$|=l#`pZpxnw4l63hKYIE2%ODhQD$w@-TCZbbdBSa5Q=~FXPz0QK9Yo%yUC{I% zNATyEM*1YT-#&hbr4jBPY%Hf@r?y;4FoZw!&hpP%cVX`GoU`7Ua#_Pa1%hR#m7R+0 zac$zKnAk_}Zg&azU=uvr@H*c8MOo?5RK%7|miQxVo0QS_l&_WF9{C4|?jzbYq#X7f zgczn{hk5F+o-&M4onPz2a32h94PCHiz7Gl`-xkI_;BQB}q?bzh9T}qVslE61t;6uZ zXBc!`VrWWJMzc70!oWYGe>~2>Y=l;?i~-m;@*3$#JZ|I~7u{N?)a5U&9Rr~$UR*X;snCwnA5j}rqvV#XL1b;1Q8o~7k5 zs^tHciuw7eP!&%LO06Ec)O6S(^ldapp5pA7Q)!H;0V?5_Sa3am-@rCqg-QIkMhb22 zpK4P-HUu6!_Fh3L|8rTf#atmh{eZ7#-hjNkVu9KeKNgKhIz)e0Nymy@z7WYErl`v{ zAu-|P3YXd8CGdFS#0itu|8^SNi;J~#Ikt98I7$B&6$l*4vRg} zfg-v=?^oRago(s6`yb@z?Mi$Jm|8*!4drn;#BCVAjm9!ld=x%SU(=sI0`qp~0U z%L2NbLMq3VO4Jv>YRao}Q)pFfHRg~!*BAc^O0?CF`XL8$5UdS+38h;^TdUUwN;!%% z4d3QKpjt}}9ftP&2dm6U(t`df~C_H-tuoix%Ddch|Y5>qzkczjOww)?H+jfeF%guDT0jf zglf+T&O#|!uT;LytjXQd&jljiI7~(dIHQz>riEgQ$W1p0hms%Ogruf2HbX||ol3uM z5JFTRo08gD5Vm=Z-|8&spldg$H_34?-fi*h#ujHyMuyGHnAqO`6Ey!^`>UI`A_H&= z=(;GYKg)QP(_TOoY8cJ9j*jO^g{MB-G!Nc)a&GEWC5<;zcTpdWm3usGMU#Fp|Jl;S z_+^=mQwiPC2ZCL8YP+V?p@G9JEF7$0ukV83Jl{{{TPy|-49|UhkdtGYM3Xg$7V5*I zgJS1i7J`C``!SO$Z%>5?4UEnMW62$u(X#*b=8lg2rUjdW6* z_mcXB2?JVnkPzJ^1)(*|F4ht=D_|UQrgNS(4{IW}{HXMjFSoSNtXtHqX1hWW0LtQcjjgfPaf9xhkYi=67G>c6E0xbL^wr5rKiky-ZrKMH)bB9A zhxI<+pfk@>QmRO?@af_3LnT?zxA^Y84J+`vE8mEkFP^9j++DcUow*LJnvo@`IFiR! z*euE+zow~t+bbdAes|&bBpZd@m#3$F%TFM2=n<%%c5_}%OR|eD8Gx$VuXZBU8(cWF zZ_idbUhRe)SPwii^yG>rxlc6$XzU4Q(ViOFTogB7!={%5lVV8z*o9eY)!R;TVl#7R zsalO@;WiJ%i424;O2=_T16cZmq@FPIu;9&%rQ=FJKJ<8TcwD{I4f`;JaC5{_Pv8y% z(rlqKI85)QHy_YYPPnBy5p_CO!Tzi90v+%#dW#SHPq=im;PZfQ`4(8wb@$8btKFiCxd$?Ri`^k#TY?`!!Y@QeT98HT|e-JVy*_RAw&G| z(;3dr7=?3HUq_Q(#z|8>;ipRhf@dxMOo9e;>9tp;?4NX+*W6Rx-95Ias__%2KYnFi zNHL-}y{6Nsv=A|Nj%%M5exHjPhRy_F-r73;@qmQcm)AUNBJ=Mpqw|zEJGpcjtrPd2 zC2$q$$Sn+h2AB;PlrFc7f(#eZC9jdH;XQ@sM1i@aC)j>GJA^JHmCAo|cY&5jmY=|E z+DVw=(L`-3CzoD*_!pQn;|pUu)^pcjcsQnplXnm=V&1R)W-vn;~ZU9FGwI&^Y%Ucl9ANPg7+Y?y@2af439&mSCz`@mC==gv2^MSsmIiGo9)t=z?zKcPi{y}2D2e@6CM zuSDdv-3Ekl{mtS|x@%M4jkK;zxKQfA=T!JwKSVrJZH950J#6@JHv8mojW5=p+(|vQ zsb#F5m0~OBDRK`Z?uJ(u+zzB5mX(<7XbLPP6nVIoc+S#2xrzW{{w|svTU9sv^1kd5 z6A~sk!!GwQlp-LKcp>*36N|*Kx+rJAPpwQgSiMmdt=^&bG+`c~s2bPRbpRXLE2(Il~`Hf;}d1dKat)ftcc|%pAjw{H*a46%-i)dHtb7(T}XxT_}oRyiSaxqx^|FX z!q#Az^@q^#lYT-ZfLOvB?09jh+$s(ilsc%XDe-GUvSaa6K|dqS_NA?rrF(PrH=V&& zOwLrLv;vU#S%>#GwUS1b0yRn7oZp{MDx6Zsz;*iv! zyC9>e;w)%m8@0QaeZHt!H6NqSfLHYimk+YDgPr|^Tu1z^c^r4!fMVJ?GB*NSqRpK3 z^aGrkH9(}pRWiSzKBeuV$K6ap#3?ZXZl>qJ_JXxQTfDib%CuNKumLs0L6S!%aC@Ib z6%Zwur1)X2Ul^DVKUv@_r11ev#>;8ZnS|Q`nST-alGe2Ede2T)RYgUcAguNQWwH)!=|ZdLmf+WD0pLzeS-kt%cj!`|B-V5L_Y721tQvp5_` z3Szg2EB8eHW-X+gH0A87B^@eNp$|J!Y3Cap52FBv5DSnkHa8lG)X}R7$C(`tf(mrW z|FCoc0j2>2+XS2HD|>v-`3GW0%Q=nh5ZC1*W6!I9n((Lu9}_S3{Tpq3?&~kd8f56B z=qN2xOr<7!Oi{h=wU>x4NB{yU&?yUE)O0$szbrzRlPXrGb-B1s8A#Aiwl4CRmaXDD zBbiA>qn|!mrMDWhYe8{#bv@}HNac1tVwS2Zpk+P`HNRX-EN0!`J-qsMWlVB76ADW7 zFqXi5s!GpFn!dU{cJ_0WCOV}wq0OBzoA}-wn!M5-5V->un{lpUsC;-@(ACdM;7vC^ zx#;P~?ria3xfQ?Srq1c<*v>?&uoXbXQMLOuNL_k*h|5ZT+jLR_6ra$pV;FwiG6ErA z&!~@I{3T3o!t8S@|GZ=yTh(#ea$1!sS!|ya}{1?J^wSOU`fXSvlPekpxM?h zHe;CWxC!QTgYDK)oebz_7z_8mr8p1g-Qr-P5i04aTMA0Mms%EB(E zx_{zY_wrH*`6m34%Ck9b7g|pgVwh@w zUSowoe&EIm-T|87m*YT%X-?i*e*U zzz|qkQL$zeS0|$^x>fn^74ONWgJ=^=i!*imrqNpW6EKa5zKY!H>M#f;a`Qjk&K6(w8Ee0`jA!)VU{Br-C zQ#t+%bM0Xz_g4vKHJyn;q>gW!WbXHs4~fUUYptl(=%AgcqD-lE2yre@Ce| zmwO$MW7Tvd_g|BC-_N=DVp?u4(ei>q*ELMyPaP#!RsZJ6Ba%r*bbi}k&}S8Kb)7Og z-FM*j5?NC33reWe%B@-ao3!{$B(uV?M?SYe@KU}Tl>(HQ|A;hV@F8J}up}v(R~&cJ zXF5vH}r+4(a9k@YxaGo=sw=*0Z&>h11Nt4hnxchJCH~GEbMnQL978 z!dkrbaPt9QOY!2%hPQzF>z(28NVK_`$tcsz2`e$TFPM-3AYNt2c!d=q$%|iPz^cw` zS@Z0F4sw~}+lnVlY247ko*v^T+nB?_?5lPKB&5t_pLY^Zb7J&&iF{w zoQVWyTDq=i&fZ3@P6%%ew@3iQb;)s2GIG?vr_CTY`)~He^yQ-d{S7i2K9vmf{A;%F zwl@i^!Lgw1JBD`)zAitr{lr)-&+AYTkDNT~>pwg#)O+DlauRGp8G>S_=q^7xfPsX6r&ft04g2IjEci1fkl z7Y0`r;Iuzw<;s_SOMyANx}+=SCJ8K--4!KN*gVOt_;gle(Igh3j{O!2xCHqv#;Da% zkaCeKE+ksAQt+C!T4>w^-7R2~a znGo*S4?H~hmRY52VsK8SMo8X!3oYcWp-an%JYlk*)<3JO9S#_&!Q=abdx6fjr}n>l zMhvX?MqbgW6bb`!0zxa%>u54oCYp+Yj%Gt6M*NdJaI|$be2Nh` zk6&&&XkDi6S?$mdQQf0EuAJN6wLKZ7?yN$`MOa4l;S0b}B7kwEN~+K)rP(f36~Buf zb*lNF<<5D%{}Fh0Ye!9R3~?-pSDMw%d%0C$ib_4&4OQrygs|weR5B9 zQAr13#Pc@iYX(`ti5~!J%2}>SYd5i#l#xvniBttXU4dXpdu=#RpLKoQ6aQo#rD#Ho z+$)6a`y(7CK>fCa2mrt*R&BoSnF##xzgrQDR5vu|XJFzDni&lDN$CL<@WdImKl0Au zSqDn$amHJ8zm(650@nr+Gwx->jg7h094(3e#EPy3BSJB0uA_fG2t$y#*Dp16{aF<@XA))a96id1A4@q7G%JyXpzCaef32mw; zy_U4G`m8mylBE_617qYW#t}*ccsfL~!1i(MzUX~fRP@uM?;~ZppW^;U-NlA+y+EjS zN{rRmR~15e8l#OL-y|m9T1qQD%?RWC?$ez*k`?L5-VJ!v_2u)w<*OKQa%1vu_|EVX zvnc(z@?o3xtqcm4lF8OKXXe;k^zwAJbXedj;(yO;DzH>7rdIvA;MstF)x(s*cS^Gc z5xWaSqL}$+9OT<@ZdDF?0SE-M{ z*6ULBH69q=aO9IhZYva5NyALnj~kq-ir-sKU#G#DaxB%Yd9(DEBeBbnm^ zH%o7N4N8=xJYCE5^bJTcZM5lc<{ARmx%|xVyQtjpGA2TK){xVOKwP5IQ$I9U6o%^kD|QR2(l5+<0#3H0#6$7);DhY>8x0p}gmZl)bc55In*?$Ng^y z(VAo(i$=^J%#&Uex`of0-_H}GW+r22Kj~qdfiFL=I>v^4X-tj-EO8s2X7$8~Rm5G` zWJ`E~@i@T1%)C?h>6hzrKc?=xWhAGc?&Fe~EtNQR-(y~NLB#lSBsh1 ziKgT98%#w>Y!UrFk^h)#mz4I?ITsZ>vXo)7wSLKoRdb%QMBIU7Y;1CMAmFt#^^X>B zIR3D0pw3S{((}(WcCxrbI{WGQcB%V$uUC)*pM?e?@H)#u91fb=?B6;2N>;JoF+fx2 z1D^0gjES}D>+E4b2L)W)uN#jw5RfSQCk9JD%|A^8{gWy zk+|g*N?j%=Bnf{Len&;^x(}GU(jIn@(~ziZbUz?v)g%fy>SymAw-JlaNxsAuNex?~ zKCUSNBcg{eO!B%m7#)R@5=z!#(dizWtsNU$uvneU!7?8QqWf0vXHYOywu(pk1wd&+ zfYC67t@5L~-a6nL&2a>a!tKTsuC>M6L6ztJrX{JwE!WT2vQfy4Hn^xbQ%Wq z_UFge!wqRvri@I*#vl}w6jXfmCyIf;UW=0_6W%DdR zse#WP@fB|0e3hWrCPSiHaW(Pyx6+Y|oeSkc1!9mOP%0w!p2+U|-wp~w@MdW{VIY3pU3InPdL)}f=?Ct0KOA9m85&3k2fteju(?ipAku7JF&%h zYr93hPW=E7_?5?!Fa)pQ-Umb}?2hk1V*UI099**+>kFdrO4EpN`VZ6#fkw**#t^7( z4Ty670UiZpAmxPo2^PwWGXn(9kiZ`X+yj>L=~)niHoskNvO(c0y7ze^u7@>2!btnS zM$O~n^REEdZ^|SZplF%J6n)t%QGh%FYRXg3%+YoX8vq&-DosJ<-sxf;*m)BZApZLV z)rv~>mx_sIoxZQ?y7&3AZN0zVVu6H`-bB*lL&t7AJ*Cx|`wtc_4wq1XedkEJh5Khe zrLf-rHkS@ZcwuwRM3TgO0`h0%Uv+l9lfiM;WI^DPFvt`L0DSw;8fU-pJ1%<2T`7Oy zY(HMv>WC$vll0a#59jwY+%u8Q`I}M!aj8^j_;<1H{~{}9b7je6AmI#dxS8+!({2J8 zN8@-81AxxC@?7{>@A1O#MLC@-N)T7!X3!yZMA2lK)pQEq)*F1cAi(5|f_Pz7Dqjnr zY1Ny|$khN~ioBSM&JQNKBsiUBFxYR}uW)bR=*Pbgg=&j6w=$h9QIkVnA8%xXTpoPg zkglr2S#!gsFhQ1^#jos^Q$GRa0zI<&UChd75CZIbhXu4r-8VYydqGv)kF&e!QwR{e zIfnzp7Qh6eXZB9)&Q{YWY~5T?Q84_JZX^M5Qr9!D6u?`DM_Id3_&*dk23JRpJKwpi zFhh#HuFwMuLXksAUE+Y#Sr)Bdb`HYIbkWG)yXW|ywSepymkXDhD+vf-WdwjBhI~|} z8UB;zGXDUmiV+?&UBK4z&E~>E)S!5aHhMchK0QS{Z_o*Q*00Nn4&%ZH!JDJ8%Al{0 z&dTOjl8!F} zl%7aaxt5>;rAWYwrODJUTJZ6SCC1N z-Qt#iATESCK9FQmzxx;C$EmztUUI>*m)cuTCZq<=x6L3E3ew~sM3X9#wv>r z;dP-jnBT$Ft<`#ZnD>Xk!;zSB0Pbz$!&%d}LqQ4#cDY{Ux0Jm%0x@A>#pAaH8tob~ zE*onG^|G(=hf|mw1y09{q{JE;8iNQb`WyqK@<1c`YFh2frJ*yctP(3VcH(+|SeeHr zo{j(r!~RrA-=sO3L@ zX>LCOheq<^%NV}yY+Y$8w?hK3Qr51zv)Qo;5eY`x-Im(MlT{((xAqACDdkNVT&J_p z)m^+DgYFscX9g`kCvOiBN?YCIPT5=VP9qnf2`?~oyU%dC+AOzmXZgl1XYU@^arNSf zrgQi30D+Xs_S@r0WB&q`NT59Y3$!D}Gz9yAs#Y{HHTc#104;75i~^C@C$jD-GBVWN z9vHn0n4t%*eO}RqW0K^ADl$MdgKL7wLpw+jNt#nzyqS7?wL6dsMAc;cK>`F900W&O zXa5es5rmJL*0#Uyoomfa$eQaxL@NIeFo z*|MGzH>48;BLl!8K%>FQ?|SpNKP)LU-a16abPOS=|yMJQ{T=$%dZV- zdc2Z}SVK8@rTkkLpW%<5A9_)5Cg$2f?dsj*W!XDgmRF>txeh)n_Q(NA+#syQv@1Why_mc7Eh0VjUO**R>yn>k)OE?CrdsU8g; z4zGOHGJgDj7kP)H-T$A^`@X4?hbTN7l~=MOnPKF9x_E#T;#WD??%9WWgHK;ZlGOre zqx;(H|<|}W0ITJwd+{;$FEb2rKri-SN>+QpJ zVa)$)_Y1QvMQ)aeaaHGJGMKjEZVD5c+}v__il2UQvYs6NXgG0InwiW|{T6sGKlw4D z9dbzHWAi(%u5OOz5JX(|ebO?MajPl7w;y#LJPjNrQON%D2A`;+4IKctpB&gbm%KEs z55PC{=X?4Rc@t()$cdAV`h*2&lDgFL&_2aK`Sl9yOmc{5&*X;3h5|63yg&34c_HgR zm1io!tp>ih#LD`X4I8zKm0z!@rusjxG|UhAj{SR=XYFj#xgPC! z4lIt z%K1uuc>D7i%5=3y)4ECEb$z)TF<$$Y>E&h1*@03$q+(?xnGX;FLNt!u>1_9CItWE) zce&BME6MKI%f}D5wNsY&rx>?YmYLd@2Ox{M?kKM&GzyHe&G&)FJ?;<`WxT9&SZHva zta6`haM{zUk10XMovE^=H1oRdtb!g-F=Bnwdp2THdH3EB^|-Y3X+Zo63Jl3Ca1aqg zDxSir3v4GC{OB7_kgB}Z+Z{r$=HKWTf(q0c3(Eiz@6(?u81X`>Ph%qI#;oP|SSHb2 zsZa3P8CI=8(SO2W{R+sal+nPSEwkcM%T?j|6X4<5p0qq$0{z^D`(v{`%MGO;@9Z@A z@7dFE=}PQ3rKLs5ML?eyFagf4ZsV7Q)eyvddGAk3**`jklSM;v#&g0gns@OT(-rk8 zywlRIH@9YEb>5}h8bGEPwR)yD`b}mUYAx=ULf)86XCU+y2!)T2kG*%ha}LlZRSSP& zY-;?0?Q!Y~fqDCplxce2pBVJLM8MW);)AS4)2*r%5pfl1J^QnjUp#huKK;yiBK_r| z*Lk$X{KQ|126y)lX(jVU8j{Gj)s~qesRHO5jNslCxw%!tV5JDXyktrEj5QstyV2=X zi**X@niKz%pCFp(K0XUkh|P4d5|pCDQoVNN%UY2x!dn`K;FY5zzIn=}x=#~U&Bw~a zDK1kMy0sB~9H#QMq9M;uGp2WX26|T7a+UTp>SO*n?9LThjW+R*Dkw~0;GwW2avmFZ zhlZwF(}q7WB{W}~JUzD^c5FFytxjh2%pYJFU~JU10wj^6J#Zm!@L{Nj&?muHXltWW ze*3T~GhoOjnTZuSDlTl!@D-Q-dCBqRG7CWn-(ocrgkWdqj@^x=zIF9cdQ6V2zZeihvDg z2gW3nt6XNbuL15huUhnRf5QNUj}8L^ByU1MBw8@!jbC58Pc|s{2rTVc1v>*C3Z(b= z_>O>hUw%GDF|!Ol-TjAZRm}dpo&KdIG(|wByvjn`afnjlDc47#jjyqBheIE+SuSY|p(^pevI76F)zT9y z^8Z1b{m*H~G`YlXE-oMdC~0~LbY34b6H_tPyi>r-qP+0R_g)*K68ld*ObJvAMfd+Z z4)r3HYts)jed5JCm+Dl$-NtbK|Dj#~Zytk^vrs}^pEaH(LHe}}RTT0|5)wK-WjY{U zCPsslAms`^<5lFW)|~n);f0s8mn@at9uD-)PH?fa=46c8VtJ}!S*_x~1hAtO1N{7u zNv-70TzxqF31n!Zkl6*6oZVd3ijQ0Sm9*z(nIfR%*cNr@g|cCwUHkZ#isV#D%9kc& z2MBm5uY*N{MX^cfa7`%~5C082 zVl>ML6Kh*eJQJ~D@sBdi&laZYhbN!b!+{MC8@h%;dsv`X5u`zC^YaWh#Rhm&)rm4e1O|*EW?HSU-mZ$A(2JA;ET{$q|7J3k z>A0)BVi#F;nWDAd{GM0A)oG6YE}8kRA&N)Z*(EPmtsL1axmj|tM-qyr1Ex+QSK}SM zm^g+I*~cbnB$I79otNYLmmDfWgbjo$cBX=>WZoUrnJ2@8SFGwMbkd{V`KXJ6*b#~f zbU2xK6}EZmnS5JN-E7s$KEiyJR_}5C{*Q%tu_fA9N zWcmV1A8+{=`qEbF)1IK(v!(7r+9v45=MGSdo$-+HS- zBg(>yx9`~f>8Gs}GKFV|_xtr(?KW;0>0&XWWnT*;CRT9x`QM&#v4(2d(|B|}lSCByNE*f9oRefhp`3|qb!c+El z_v|36{9IyJD_WB$p<` zM}d)%i&=ek>zj#yfh3c?=W3?ESWZ``G0MLZNwX^Wnx+&L`h2Bot`(xn=;Rt-IL9LBiM4i}I3P3xzDW zSfDbaB{%RUjbEh+QBd$sPvOS?mi}!0Az(uSaaEw``~e;|*)Zwz%tJ$w%R->CNT$2_ zmfXoc`0A07+Ty(R`82}S^*7GM(SNK5+wY4ES__}6<#7@-$xSzz#)OBr&amj!IIsSY zy4>B`l@Yd0Xy0+Tk(Q_VEAk8T$GXe>=%>fz!@8B#MS17^V}Ba4OiEID*v!WUk^-$g zy+wB;*|gpMYcp{f`gp}u5!*&|{ZhQzJ=YK*pOVMX>B@ye8<~aWMnlYQo6?*XGb>~F zbU0JojbmfPZi*6<{}5?1h-H6bI;rjX>mq9zy?4Wgz|JWcT;WpA>0Y_mKaY-moKJJA zJlQ+6B|6r{U2tmEg^2`mF7Djd1;ZVd;t|vkxeydk=o?nzyX0vvJVa`LHMO}%F$Fea ztRj>@7gT;;R*;vKlQSCsg{JkmmFhm7`rb1sdA=Io?QsaaZF9Ce8N>yT(L~=(9||h& zrg+?##4u@_YG#e^%#?WY8_A|ZCmRB8HT%*R0p7Ab&%lLtDRcd z-wvhA=OYpv6IH@I+nUu-{K8CXFr{van!$stY)NTo>_pY!1ZsWlRkVK-+Zkb&6Finj zJF&xdtXw}W9|&x*Xk>0r>=i33+i|VmTve1LG3)Qgv9)6ce+lz4u~S5*NnJNgGSQ%(1z9)9_EtW_xCi!w*?&3$~%| zN(nU>wa}_17dfa=F{OlN^sdKbWEde7xBaqYO&G{@w8Ur9`m%MgpCsrMYB-oU{;#x~ zMs0{kbk^JBcBa~i>CttE^7(ViOUDK955mPhEVI?;_#~3HENo zI5p5)0acz86s^EEIJJ}>6%Frxx-l>bPc zYj2diSX&CW=_zfAwpo9dMN#O0bM;GQl(d|5lxDTQl0tS~{My8amg^6iXEJH}#bA7g zS8Z_3u-C9tY8~7bGGB_HU*5zouRi{sn$B;}d^1N{42H0)B^lg&>m#vZJHM?Cs_AYV zx)Uxs@R)4{^nwy`>AEKu>!Zf(16S8ZeX&fs1$z0)birM1smj4r*eTFDbE!2f(`ZKP z&aSR9E}n$&3^9CqFETPR2JO=4(S8V^Ps?iIw2mH-QTx4#+#ZYi%YU7`hs{>E??`8= z!>pQa0=bqv6TQM-KWhO!VNQGfU-2EUN80}zd)YEoRc$a0%@Y%i06O@4lgpcuaw!cu zB(3zQ*H%>ANvib+h|icR(r+GYz6{x9RDG_wUK*wtTo^bP8ux!w{xdLWseLP4<7eo> zr<+ivzgE2AMqXeeW_`41@;Qnz44`*XA;UxYT+%NMUS_VZ);1aZzPG=lUQqf-lqn8^0-T+oy;5UNO)XC3yRICXi@l{}qgqbJkl!QoA`;0wE?8iRts3q* zA+C)na}K*>MILUNha$ehLWfNr@$WtPKufMDdbOd8xfV9ikComFPS`X&nd|QBFn+ho&B#~e?`ymSlk4n-vd3-CdR{HvufTyR zdA_p0KjRNvt~74nCr~&UMy8wjaWx$F@C~}| zsox{`K3BO0&qb z&=cgLf_G(#7VB7S?wqSYUp+g1w=&mj+d01DkEK$7aOsy)iU-EWBl9Nn(uQg3wlceD z*rJRe5E6cgk3!1B7^I}PyESgz=n%r`{8;MyRI!4P>8)Y)d_hp~!lJrKH1uDW`79-1 z3eqcR+=&kVZcOixL7;qmd~zye-`g~0da6Y3_tqJG~--Jf`5(z$1g^ZaF zz0IGZqb{$E8G*LVKZdD%srYP*P&C%t;OAG?Ba3bjKKj`{MJnSp$!nY!Eq`;B0t(c} zs}_ve*H%}**1M<`s)hhHTW&Z>N8+)^av>_lw&O>7V5)4BUIyV~t7nJ?Y-;-da2Dm^REczoBm}-IGo|S%nno+B1La=1 z08K-IH;u)l(1XkIt#CKL@=Fu>o2|#++6l=pTc;RSP12M7y7zO-UvXcbMAb;r{&Rgl zqN1kf^j{-2%ISJ5T~4cop2MI5I9o%tH9gW?UQCvFJkFc$7AlkN`v0PRQaYkN}nGv8I2X=~FbSb8jkCRe2 zNrLdhBJTH!XoS5OvpKHxsmh(7kHvz>OdRpqMK)=pVp{tT4&grMu3UxcD|KUgMA?e7 zvDsx0q`P*|attlJ;M#d7Nk(fI=csQ^18Z#Y135v?ioH;oC7Eyz(~(joYJvMJy)4<3 zfua5R>#ryJbxu3N6Lk)bCgt;~RKcWjsrWiHWw$qrW2-yaxf+FpaJtUkpPKL5E($dX zE*@@C37Nm!84b^kHG<9o=<5DXJjCD-{8~(R6Hie0=gTHe7=Hu`N3pD#eN>8YO8EzW+l&CuRHG>B3g` zf`w@RJXe|S+OQ9&E10k|OetG~H(TE!E%&bEYlkCHZTXK31 zy5LLg=07W6HXm;2Q$0?Q$o7y-K2ewuKmVx2qZkVd5$+xDcI%SmpwR@Jb%i3ai4)(s-bT`(Cc`Slsc z$U#!M%2=u&>Sck~QJ~qFpv~*Z?77yVNN3`ORl4Y(zkG2ML}6Q7Ye`6} zM1zrNN2c<)_s0#%P{>5(JIGOv{=RdR%Rj>Dl*g%6y4JfF&C8iB0ARq8fIKM`T-SBN zAA=ey#-%|+jt;EII0Us)S|ll%Tv;hzQ-h1q6Q&%XeN5?_kXBqkUlQC%yL%9!e0)Yo zswicG+1~~_X-CCv8w(H$Bmu`g%Fz*UW*?*r25yClo)eNna`Hww9~*R?$a4ezi`^=%&szK0Z;N-(0UF?@T(C-;b{D$dvzr z|6=cX?8}N@()B}coN>xK447*S#qW+rK74}_lMd&z13m&1XJzZGf6TLKYON-toC<~P zKwDCmiLRJXp+G%^_|?cG%X3}&8jIy`rPlOh@&3O*#1a+PuzV=20$fzQD+At`EwtPU zR`$69-p6C*X<%fRSX_l2BO2LDP!}sgNxQ5$0`pDkM`xDDnO9$h6#rofLsryp zALr{&k8=9lFjkiB3{!iXsP(DMTT5QEeE);V>HVc}Dzh-NaB}J0%-no-b)!Yl;)-M^JnT<%$Srf#DxvZWMgJo+2YR!xGymfZCNG6hX(Y$m$zI_ z;#`JZmQOcZshgARmM$-)K*}^V*|0R?ZjDV?fl!y!ycmD?*x`pLS+q}a|K7S%c0ne8 zD_aQ^;|I6+KW`ZsAh;J{${Ll>bf9z?YnPA;OpQPxfVAlEi@BDaJ@&HGQAQzOSGdHA zAqFsPB%!XW3i{^A1c{IYg-pHO_qEbGZH(#O^6ULZdYZRg`nsTlMMp$0bQ<=o9XY-E z!0!dLYx6viR8s;?OOcvCqGJ7pmO0xmhXMK7J=v>T$l5nyO~vGtBuM@u(k%|!(9hoi zlyX5Ln+K$C%^t(jbdtCZ9)dNzuPk<1%CDO-FAGPNs$M?lo8xf{k0BzRVwZ#Vdedv1 z&BAPa{JzxI$=J*FApYoNTmL44K#=e4eFV*sr^OYQ_5WlPM!=>hw%pA2YJr5w{NmZV zVb(9c7%ixXqbsWhuUndOPkWtJ2qvq?&J>Q2(+QXLG<)lX;D)tIvTySu-`aITgu4Ir znuY(OYj@mobfv@jJ18gJo_b)C?AGfcWIUXj1!&3lZ9lx1&=l|4eJli`Tqi9Hi*~Cl zluy!8c%Gp?rNdZbuv+=+_vp#|hB)i`!K-_v3i(kz;xZo+OEI_!Xjj!IMG$MD#*&6Y zO18AsUs~20D#CX2^k0?zq{#^^>*lu<}yjXl(yWftG|K zOYE0W&X$B<@I^VFO=x3>k_DM^mn$i{I$KPg((|W(1V;s6=)gfS>RbooqIt>p^u%^6 z5MMaI=5b>t=Ri`>aF{6;LX2P1M^YFmihDEm+8Yw)s7Lxa=98+c{)IUQd$1OQIs2&1 z+VU&(a8Y1Yp(Ojv7r>3=43{v2+H&T#zFtK$Ar zHA{|F0pHpAk&K?7Aeo;k&f>yf+Yi%L@5I@oX2$ZTYKE|*sI7d_uyt->A+Zywg?+Fw z3f}PPkt@IO?WpbOa|DGIMd8_1`@q=b@&eS-W0+UA{hn?7xp+X(6d6DFGsgYJSId9) zPVKf?W?No9?-}xU-=`|n@Vy&gpV@ox`#)E+8`N?84l}o`c6QJ-Z`%%#6vG;#JZ^tluYPIY z@eLuV`d6}Q&T6di&Lmv|D06kLX_Gl^RV-EvILO>5T>4!@pFmbB7;+2yoth5N}>bSRjcja0HhW%1}&yORc9EAbw8VjY0;ewmZ1QyB5#F zX9|bXB*XKBAhX7!BG)o(yFl>~zr&s%*%9r@auLB<8Y@e7h?=obGua7mdNSJ9qOvJs zuO8og0TQvj_pF`Sf7B#xJP+(U@FBk_NWf%0zvf2wghFEHQM$jsu?)lK2_ZaYQGiK8 zNYLFb_ai5mIM~$AW=&bVzrMnG=>vx!bV{Yfw5c^2#j0OY>beoFc9h&r8atszSE%8T z*%du+w?jH&UfB*i^+9>j!i-vBip#eom1@9>XB&@7Sc#>efi@P2-BPB+2FX$Bqj#HG z&>@9~UnrR&0?**?iOw41duL0`Cfsu_hY5Yj5Q_2v8Yz_aTc}X+9jyR7Pa)wR#XZaI zp53s-zgSL+skj?g% z#lyT<@xi{*qUv0TWRcnbM#v=YS#3KzJqq#I8g#-Wbd_+GIoP(joTV&2B@V;(%M(7m ziB^zTW-wuG1{|QoJqY?q3pQU(7E_$}r&i0K&CblCKSL}k;NHt?lPXlvCSHq@DaRK_ z(Wx%(PDpQT$G_;p=l+(Fo|` z|HKf<^lxVFA8sRVax3fpi23dpx-zd|pgTR3cn7KFEgPHEVIH}5P|9vH>d=`eHhZ2( z{LhFme!La;BnUc0kkm2YtfznXwV}~?ZFzcDL+vMd zv&>2J*^3#yh?}RUntLJC;uzrO=rCH^lk5?hQ+TY!|60^p@2>zkJT!CRyr{H%ao^rO zCN1f!I)fG%uI(T`Q#$kq0Su&bNQe3$V5Cg@1kTHHev zRSQ3q!~rtnktTHKU#c$?zdj*6UuEU@7E$kgB&KkWeak6d)&2}wE>nMBiTP1{1bsav><3yFKU}h(zjL>Wy>1iNoitAa zV*`-Ikbiu?sZ(07if_G`_4X2yjz%?8&#LhsPXq-w`VLA5~Ww(?v zc#wy?wIE{?q&zfHAQ%bnZPs4U(a}*{>N-sOqz^A@Co?_ceO&)#FijNu57W zOiho6OvM;RraC-ae(rj$B$OxM(~8eUuDfqjuZBAi@06*HkBwl|jN8R&5_Zp1U)NH? zV5!-prRW95fo*mR3xoBHW<=aFX+Cn};01YYk=|KjnB(=!&F5jDB`Q1Ru;6D!umuXT zr{r^8o(Bd=$9O)F_0FlgxCYH(yA5+ecbcH8Y19=JhJ8h9=OTd?;0rbnG?AJbJg)jv?5= zL^(A_Xg@to>L+}yuHq_$DPMaChlqL#8|5Uz;3!gT3GkdxtqURkWoOipn;0m0fJpb8 zQ>Pl#|G%NgLIssgu((4)Lax?CFnrlX6?%z#5;ZAIZ~tDrFgH~4NvpfdJPzh4YEjYD z+}(P4p&O>G(z1(-&|Ov-CQ&raVSL>eU7}G)79xa5M&W)24;NGTTa`fnHzyC(c5l-7 z&WH1Gx+ByoxYyHkjW#c#5MzFYhtO`=!-{pMr-GZb`?~}8qwr&N>Tji=<(ZAp>+0%+ z3H>mI#kWmOO|AP2#}x0h_fIhdUsd3xe;al?y1i#2$fM8e;$5nN!-sspB;oeD7=O4> zZOn0L9phy#c8>o`R;*fK{7N>3kKp7zn4`b5*K=kw6$Vm0Fb3MipA9Z3A1$_qJp(UP zJ&L7&dujH?MLdsf$|^(nan#t?c-X#m9vB+UUxCJQETQ-dx7O~|u%XV*s0Rj0zYy>W zVIHk3$*mqAx>K*0MzEEYu0D{3Ard`&7_vl}e4~H}4jeyN3456bx}<5=e>DvH&|f`xhy7g3qdRgFsw=mx6A=pXTp?7grqfg*h!>@eE5gl;IK0KjZyoZ!6f{coSYPuuRJ>Hw+jyIA8M=fGi2<|FMd$cG~V{7p?ZCilfwwa zgLc>r3u1)A+E}T8@U*~=SLWsTyg@#ipTz*9CaH+wgSmp{sW1G5ht~M8HBEkD;kh4c z)4G_i515jant}$Z_Wz~0>;7sw+2WxJ1nC_T7GWt00Y!>5=~z}^>4*WMMpzb$4?8jfqe)u>KJVliOP4-g8KU~~f?a=G&1peO?9svsEqFSZ-dD6sGyNU}0nNFr=mO;-CA$RG568d96*s+x>OZyOyZCa#!aEeP?-oYmZRlx3Ttfw=>lje$$!a zeRLGp@(v|0a;MKeHrBYtS_7BEyaAJsZQU00BIe!vcm3^Rr#dFr$VtlJyjm%v6TCbt ziQAs{;$mB>SeH4OczoSpoyeiym8pL`{|ux24DO$v7N{Bu6*2RNw<*gu`-|-%9iS8S ze8*dhWtUuBvKM0jUw&GOpC{|uHS~9rcKQhn*M8^vp4CAsLfWOJeK|$}X6!i|OwIE2 z^hCft<(~who-vIzwD)OFso-#!?pWQ0MRfEpot1-a!;Qn14z4%}&cIl|!(D_#B4oY|92i#`^0zb2kM@%t@AJVIrBV%KbVWQUki za7Ni>Xs znSj_Sc4y~au#pl_Vy{iS(mfZ|J+~!XEDf9#ZpQS-uf!kstjec zU3mDtKuJLhU5dKy1%-}os@r#V5r$i#;^UZY&mD!Vxd#6Iy!lp)dQ_ZL|48<~t3f8N zCo6utK=`~C5Bgm7y$*!M(30UvM=lC5T(}N!b#l7jFAWhn4Ke1o?T%=1W-I8uEN_HS zAye9vIq$j`lBGG#>J_5l|l{O&l)wCNaPEi}?>4geEE zBlk?i!o%#q%}gU3MnZEI=hwV5b*vVCP>?z4ba$%c>fic-OLw_X%7GI9X!gpOcj=Umian!|!VqK{XTXbXE5 z<>c2thwOWTY`ss<=2aiQzR`7b6SL*M^eliEkMt1NZuPotTV><#%<%g*$#pYm|ZPyT2a?u z7MjRVwDTtZo);9YZl)RQuvCof?(0?Mw#*;6bDFo0zl1LpZd{N8B1mm@3aC)^K+erD zI?dAm_>+!`r4LW(#NJajIkCEm`+#lFigYtEnwx0iHuFBzTIkXLS`C!;DAgD*sw6K& za%aj+8;{{Tv5n53Eriu(6G#D638u+t*60O?4k*R9r;^jG+|~CO>L1wnu0%U2=z~`L zx4+lBdYeMw_(#$u-cUP9*;c@bAcVNB{s_99SG+suz-ww4pox17jJ2r)Fim8C1#g&B+6e>`eh&3zkBqCbAZhV zfl1`96@ja?&yM3K2SZmr*4q&m@1j=mSk*-r4|`+!is`l2G$ZYHbf~dDgLPZ?aHFMA z?&ha0qKz^sZHp@S<*RENA!s?G{)Jdy;xeumD8{XLxya$4Wd)=^OC{t4P=!Kj!UndR zvx-e4XC`iM;Y4V23`pKVkS8v(*m+&dWaz6I+>X9L2_XUJ9)=4_o-HjkOmf{-? zelZl>WtqoZ!j64S<9P+KqA2^5m91Fi`eX)QJBzQB%~uLc*uWfI%EIhpRDZTz4J|qz z{)?)B=fN`_^@Sv~TV>TuEZ*A+r@9H0UlDlk7PF7pB_5yl4AJ%=wR@wUyZ!at1NGda z>cx#9x~jpi%(BiC&$VgEya+2iz|b%^tNyzId( zQtkdZeS?6T*lUIj4QEkn_h`xz~yGcHG&)G?l& zR{;*bTj@lpj|S0T$@GAegR-8vmXyu>*kqKe&8IE}{|%C`=-P=oW2>{5NyJ=2&x*zE z`B@+#@4}VQiIB9_Br;|kovL^sp`^CzJ3x4zXpM${;kQb+uc^*32nD;q6A{$yEqM;I zoLM91zVzzVMSlSv)YGT#lBAsU#e4;g^1n|RJrJChmeRru12k1qE?PCA$qZq50QZyMp zAI)5*A@69p$k*#|89+Jl1tAmfB`;9pd?F8#2JtKRvMU?3DI2r}dQHD=&5$eovTge9 z#CBCE1q`Jqw@Lm(2c!rn7!ZsE29jqmHb}5YETZUmD-z1*taT9Tg|$)USCLnkKhFfO zS}^_34|ojxv5{@0RWlp}iL?cQeEid;d^8~`Ib8-l#H^DkziA{R z+-T~lh#Xx|2FQ&O-KzbLR|&b{FH9nJU`b$>xP#Dpe+8a=5M3l$5@a2S1F0Kmf-FVB z%*l)tkXDCK7ZwYo4&(M^OBx@~FvfDt4u$X83DjnIp(h?(iBC#$4MC=UTOr(zaRopF gAcp^!L8JOeb%Wh4qb*k91OnP+2WR_Q+rX6n0T}FFbN~PV literal 57400 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 z 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 diff --git a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/stream_message_deleted_dark.png b/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/stream_message_deleted_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e59943949c9160ee22974f7191843811789b346c GIT binary patch literal 4536 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 TgJ*f*H5fQ`!riIf5t;iBf&*Yw literal 0 HcmV?d00001