From c9e00d422e585735811010b80b25a747c2acaaaa Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 6 May 2026 14:56:13 +0200 Subject: [PATCH 01/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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 zcmdRVgLhor8*UogZH&gY(-@7Drm=0?wryj=#!eeRUwqN?L zFHTL^9_D#p9tuA4f;{7tWN3Z$2BAYN*zI`f>-5;cDfxrqlO2IsP+vJoNgAY`udwl)JbSL^8rOcJ0 zOi3LT;rC!92CsyOPJ=D8&xjN;;h!S<9lc*0w(H)h%AVE<20zRe&2%%n(P!TX-gIr} zuz7QeTb;;?{Y=klk@Oh{f@ecT zOQ}5OSHx5*{-vg>jxbzhW!RCj!UbP`Zkm(vF~2KX4GVH$+Oc$kf!ski5*<>0E8%*dB$v^HPYgEXXeFoy-m2eB5T)9!YF`)4J^2?a7{v^4`xnnBYa&Oy`yfN;?`L#TF4VVEY23 zSqxC!>wSG%yk^tnR~qVV;6_{{hm+lJgU zW_)RwPj2qfmwTtN$6jPjUsyjk`0ACWDv!69yZk9mKoBdXJR6-!%L zlZp2iZZiSm<3Z?=l*)JF#`9`w1=gqH0{VMG72O{CYvEF?hVHuQP3K8+t1$%YENc>X z;Bb&qYjsn?K*VJEVYrD8y@B)iUFk^MT^FYmn|54+PQqCC-T`L@>`qvgmlt91g@)>^=G2H6b0n2xVG;R%S4#7FzetrbDqAwovto0A z4F%D@2?a>RdI?)8?9pv?eK5fHnv%Gc{tA8`StXh?Wf{T|5xNM<*%%^Kq5mFe`QRv; zQlq&gc=!cB2p~E~%8!$(6H9a|NgJ;8R|Fkq-)LN>X&8io>q}~p-DA9Q@=qZ}Ifi7e?bm6o&RiV8 zQViAa$zlePD%MsQenxRih_>C*$B~&-f2ncjf(NzcG)amdS}u9zME)`(CC?WDU?G9T zy(Ot$WQ8V{|Kb{gyYb{viai#UbdnRjDu6T_-DeB8AMh`LjjCwzT%^trR;&EW|3+CG_z)m1))C^@nDzbedL3t^?C-&bGnwJ@MTLh z+5x!-zwWcwXzRG~&E>suu!gx%@LSgAa0c86`@eu5^S(a44KE}zTy?h{{>oUQ*I!Bv zddS25Jsi(8-aYHw6K?1mp~p+S##Eh$8bjn4q1NiI+0w(>rj~S9sqVP=mYML3aZqwz zXr&GxbIl;vNw9;IK!2DJ!Bub~r0B5{dDNGcH0<0S0eVmPr`;1@Z$(>-DwNdQbZl{M z>(c$qchMF(ujlqP&~o*Q0i&K2NWi_QR!?N$3*7f?Nkc|G@zrSNt(g4&qsuj6lXP8f z`@k=jWzw}C2%YMNsdjAiBc^RaBb%f1%=Dcua)47+AvnUqmOVi@y< zXNuEETaD}w)8D^!r9Bd&4a=9RK4LU(NhQxI5~OoB_S>3Au+~0q8f^TIXA}u!aeu=v96rvL-V?7SOF1r}O~BMr8%S21t~X;`;NFB0-gMn8a6H51d3LcNhRcFg?wys;c>Ih?UH6Uo zeD;MNiQQz_S4GS*VXlCQ$BJ5yZa?i-^2V+sSISPW1x?N$B1(0Iu|L{VoI$~#Ibig? zjAmB|ex>-++}pfm34&EEwwew$Tx$}q z+Q@rZXD0n|j5|I4s?=Z`ZpvtD!(Gu{y*k<8uxv#YPZDf-M42v4F(+>9NGED0_zqh72wf1u7IduwYR_JJT})iLRTD+|Jrj~#J~kE_l|Sl4e^YD(tXZGY zukgUTSYZ9qs9@6Ny2#Rt7(npxi)nxkQZ>xo!~olF@S;mO53+GStqQ0+`^bCTpjI2< zpCWBfFFis%nn=2V=Z7DB(6_*V8)dByy4$U5x0-6bl=YvtZmA*Gr;#z4(5bO_*NTR< zAFP%Gq&(Zum$4;s)jF$vA+dM0Xrb4}*&5A&Tw1K*6deE=p^2+D^>#txTyTF=|1*vMmF8!Tjy?o zt#@oyA^OTwJB#B|&bSYCUk5@iEigRG6ueOV>W<_PKENADw>xUZ?QV@wxgt`Q*L z5QobFXhCO4T;o2?lnPJ)cj9^((kyHy?6w!m$($m9?wtnMX!mpTQ*9#3NgoR_NwzOF zGi**y#*p&gB!~>+>p*PH_mz!cJWNu?q;;YQlQG$j(%|ftI6>Z+(MsnEbOxMyB-$;!Ij=+2+4zd$&T8yZoO#y?Zt^K~yh z5{WGeY;0tI)-E@ExE*(h<8%qP?V|>D`om7ZCfo{R6>xx72LEcf<5i)JWZ%inhjSS0 z^r?V6(=L{OSG-Q7!UKI`gT`FzwbTl@&jUMs@W_ZImbjK$4wVy_crXi*h7~f7{xn(f zj;R`0LF}h8;LhtAtk~4oXb6hgYo3PJBy9QhS z#ii@UtZGf&h{N{HO_GvNGEgPiu7nROBu|m;w=+1Kct_XrBu=p$2XLnutPIu@G;K__ zuEiCe>iFJshjmSrhiQFdv@h&v2ojDo@o(l#t=3x=k*8QvB)HLKC;zqA>gmXQ5I2hB zcG)~mAXi}XD%VWP{}G87WfGSE#V~}miYm>PM2s#xw>RsNg|m17y&Tq-(@qhwf&h0L zLX^FWK;JZ8H_$=2qYZ(;QZwT#tcA#6IYI`>4`HwH-w2!vjM{ji=t7*YAK{boot$77ApAW3^F#vWGwPPx{Csdt~TOy+qR7p+{JPuQobHS3&z0(sv<^s zDi#IS;{1&LiKLP=XP!UM3usepF>Ga(G);?WNezPu#7Wo_dvji;E*+dSPv__@kk|5J z5^Dm}Jd2ewUqm0Ln%Bc~kA`F@E6TsA%!`+oSCFppd@J@+F-xJyK2bq5-^VHsJI8nd zXs?{>0_6xhw=v{F@AsES!(oy9y&(rwD}3HEVH)@yal2mUXhS;eF-ngUT+A0kN7Oe_ zH{JMS*bh7U1z0dZUUH?Nf=o%bZfI5Wn#L4v_R$9z71tibM^4>JTTD#@kS00i7V#7a>j7bf31Yh(uXAMNN$smYj&)sj2U4L*v-IgM7|Xw+IVQ3Ezjw9yFwR|>!YRS8Z3vcEG7*vVB<`I!_p3;` zdmM6Mnv8m%zrAli%dJ$z4Rp8B7e+xyIp_>^`s@Dn&(k0C%g>(B3zrw=K`nt`JX$zx z;2zV?&!hY@tK}|eig_0mL%`&Gj<6pp+rg#Q4~c++6%+ITVy1 zny}{5Tq{i(#H1aLUv`?s=8!kKrckDB!dw*R!M&=C{tz>dGXY~WBVcmAlW=5%#9vaz z>iA3H=$>#rcGfv=si1HD@Nm-1pS+_F>MU8P;_)2CInXjk==kBkT7PKK0hYeW*`8%L+hGWza1ZC~6;x346F5)OTOBMcH;S&W zp+xSI>Pi>cGWG!<#Zv3VlGXDs+4yoIg{9pBrhhpG!`L$eoc-vJr|cehivp+T=@KyM z53a6HWsh<;hx+-3P}%!cLQ$la^?>1!CFwnJE4vKP`|KJ4*4EQ5O@|*V;>woUN)F@kD&G)c!HWPo z(Y~EqvA_v8MecfPE;`B7wgTmTjhl;qZ^sMn$>ee#wtrJk3fWW8b&RBGU=K1g^vM0J1EEFMMKZW48?g< zYm|6S_b5BODak_8b5IHwRuzs|^wC-Iw<*4O1MBc}u5<&p>c%8x3goc|P$gAV8bO0% zm(@#qwVeMdK`#5Jr_$_@ZEo-^HW?Az`onM|N(KjK_Iw-{o7ZphsAEV>l z{lj7(x0lTNtMh}FHO?KgcG>WqtaP>#vvi)3=9Ba%IEUip*< zw6KPpcQEK1VpPt5M>#@73D$ie83=I{sjPCv{1v{BgqgAHZY0+RRc!1gNi%jDj~r_Y zIF`kkaU9@;#-0xyuvk`i52U_omqKI97ZCbU6Ww5&(&tcA@p|FD$$ zjttJeRLFc6cU}o8E~eb__cKdFEiW`O2yJD&f8}fS{aE)Ukm^wC0oLS1INE8gOrg^V zia)|qYlTKv+ApyxYebRMpuh!UvhDa=U=|feW52b~q6-JPI3JAJl7tq5O7&O9>GTSx zz{2Gc^`2~ZUHcyuP4d2zX^O=}h_FW|v8g$=r&)`?b+IHk$;YKIUu@OZq7jq|>%7Np zwnmkiCSmiy=}&R0t~aXy^qmid!n;$M}fIr^C#Sc+zBxM#wVbSk5Py zQzwpSNv zq&qROp*iQUytRYo*?s{e4+2fLOBikb*bUV7iLg`-H@rWivDQ0Nnn`eawGmV28tmV4 zH|!J(vAShRlo)dK!+%$@=#Z=g4M%B0TVn?bCgPR;#d4>nM3DO=v&d$m?f{@1rM2&; z@jpE?`>}6v4aaVFA6=^JdE#w$ReGRZDX|0y3KPh2S=D4nVk5pt)gQEKIx?3iojFt6 z2ir5isCAkTr8F6^vkoa>XGR%A!zAL9V_AI*GhjTz2su~MEt^AE;-M!Ii(Q5*wC&Kx z5-z&lJSp}pdvOs2-WGO!-jQhBT<2A{jF8RBR!nzBgkTfEgKtTTuw8Y?f?^TKz)kP*M!fGGwa5>znFPLw#v z7ritY@^b}MZI&!iVeXsaG`}4rdu?}|lJPTEID-67`;|zSXo?I44EPcO$5s?jKPR*d zg4TgV=6ya>N!5~Vz8BSnQY?VO0h=c?JRotL_$pp z)XRG7rXe33QIr3SXKl|1b3oEGdn<^QsU)*6z2Rg^%g zyzYpjZcO2ec`9_-$n7&~TMmo@B&Dv)Z}W*1cvoXtU#HeuJHJ;V!U|zCB(u=;^i~+xL?bA zUB3pZ+wBNx#UOGIZ@-=o+gBsR=x6SfAMZbA|2wb8`CSW49NO#-G9U{~(L2%-j&S{O zzj`~fMB>jR%aWVPm6_4|HQHotUt$*z9MfZB+ab<;qV3p>M2{(Bg=FQBhx@e~>D^o4 zsu^RRuOV%&jT~q&w1Z2X!x|d7x)6|i-K>ou^|L<764N(o8uit_etOAa9T$-2!%zQF zX`%nXA)gErD3zMdwM;lP2wIn1x1+ZI#}*P~Mr8zOYs>cFF?P?rvr^0@^|c@%2>-LKY%UQ?#MZaynQGo=R}19Ji5ZQXSP;7mCr>7ZzyIDsDA4C(9WQVcmgucmcCb(zw3I zsWJQ)Pd8je-;@OG%>Qu^%5)rF;u=OL{0m2?Do2FJW5^?*{3_e9J`W)(Jf1~?^oe7- zQvwU(G%i(AV^*M(Z&)lM7s!+5Csu38%NP5uv@qC4Zf7z?a+r9e8--VMRmsT{Re2n4 z`|utP`9(Pm#>PwQ9L2B>U2fG8i0h%scjrifz`t7AmVVQk>>kvhXgYvS@tXiMFI5^E zpsaUa8S|;mgU2new#EobF35)N|4vANxo#<%N~NWbO`agS7@Ceix;Am-Tf4L;72*?= zh#tMVJ;l4_R@z=*kNT<*aWhdZ#NvK|C0i^l7$gUtHNQxFYS8l2H)?d{lx8SQjJ~!4sZtGyu0JriF1=gD6J z`7;O|f38fu{&gx9o_p+zwcenmM+WbWOHlEMbKm^rDgvd3{s^)AXHGk}PkI`)UwF2R zF+bBn8jjIYy29KkPm@eJ&pV;p^M^e2>3H^ZHEtGw-R$63le>=eYj*2yM^F}@2&q9a zucKQQA!%m0`EwMm46R!hrs2VZ@Fl*=N`DKbFOVw|6^Zr2S&nBZaSp zr@B}NXOd;+n5zASkuHn7wawculX23(F4JVJfBs*)798e7Vi5BMB_F$~W2r11Ilh?a z5&ANWSDqx5O-i;TpLEe5U7j?B`y>2`C`4?kEW4s(y|`qR`QetL5PGN(c`Wv={b%%9 zs+Dya2P04YN$Sw;Reszhw~XOI!KTi#2CIMM9E#G7Af~Sz|KKzitT5tNr53BObaB87 zp5a&omOsm+B@BG(vN84E&KrGjNmi93C>irMUzkNL<}KV_E>PZ>SH`n?1cT}O#&S9{ z&Sww!LgM%XbGyBv3V-e68JJ4pe&smxXLYMityxAR4=E^8#%v-C63GU-PiL~Vz?K$t z%b+JAoRvqP3zKapS;&gbmEk$h$6BcFw&=7Yp_f3wEy@Jq31e}&Rl7D`s?TxI(Ti%d z$oHDs$g5K5u-!x8T{GJ-GtI{)Qr&d7;e7ur(LCl5McV0biWnW>C||)^&5T+7 z!1NX1I&PCSkCwOpF_?jHF1OT;Nlqw<2vrED#3tVbDymv2Y*3@djVsRN6Ak<&5W2En zGish2419Rjntnqwz5;_noSCQDCkm6b*v_a#Pen8+PCLM9WXK{ zXqhk=$GtI0wxvC^UKF4@ClogD>e{%|B5VA# zRI&tw!h7d&pZ>p~uZi!>A*6&HM;Fm38Mo~ufxj%~y5$wK3&=Nu)H=zF%`S#YhAZH! zlAA9-5!s)KRqtW5DhhSdqv#|`s6@*v5hoC_A=jIwPB}xU{ZSd)Je`53s=JnaXI}lr zZbhd(Xfy45n<|nVmHm9pfWIZV>PtjP%?LO^Gdh&;9Qk`aeev40ALRSUp??P>C%R7U zHhRvCwDdhO0>;G9%nx+ku`=sS8*L7XRpZKz>rUah6pFn68Q)cBNJwamz{AG`q+^XZ zV#lEzmqhCcWWKyBmOlK}zYo(RG&36C{9q%uWYvBKK0RpB`Qc`$k)K-L^kj|a+ zhAy#)l6vE{LX<~h`{Dl30l;4A!))HgzQ4HxVfvh$#{HR;NouWXfO@J^mziLW<4 z59o(qM7PP1@t7@(kQ0Rd;|1BjbfY^IjiKKX(hA~73h*WI%=x>=RyMjWm16xp`MgB^ zBm3#bKT(UHg#P3{czS>{BnyC*c}FXX^*Bo=VBPq@>y5#AqzJkk0I=|7BDqtC>h3I4-)&K@&-2QdSKI9-2iSYv2lY@*fA| zT%l}CTVJEEBZ=@D(MVqd1H(8Ob_JjYkmXP=w~SgE-Sk_0k%4pph(8NFdXQP$_}r09 zhe8CJ;j8ldB}Peyr#HKwpd-PLNl3$)jtJ9+Z0{3dWOWZ2)pCc_EH^@wZ~B6^nZ{nQ zs}$LjM!R@!PS`v4RMom1sM}bFeNKLyJo0UhU-rP%6Q!G0{cBj?o7~mXuCw9%Jra+{ z;;urL3Mk{2q1*ULicX3SZ-5JK@{p%0@8o@bI7?x^2(#RwG48$o24+c&$NN&Dblk!>Xj$DrrvSo6`CXosDUN!2^aze?JQ(GHDqo`W~r z+;aHxqOP{y;lM*9?p3yjgIoWIu<)=^BLiiO*(xh!w#zOL;&?)U#LBs2=r`Q&*c^Bw9Lu?EQ1#F?pqIESCEGM zd6o|tANzkz(0cHh6Ua1B&#@r$t*%<&a;C*ybQg|vzk=n*hOM|7{YfE9eW-Q`SgDi| z6>%OR&*6r6%>SsHYIhBMJ2N4t6BHeH=>SZ5qd2B(0z$Aj*2+A(+MQLh{)docQxqny>$~%Iy>+IbiflF}{ z_Rge(VD?8xw;gV`oR;!q^;CgMpj@wzPr4uGS4Wh{D2@qnb>Omr*Up zhg~VxeOc)Ss3JFry}UY{FBYns^YYmNN^5Y|8k1~E7pLHRG-4xqci#&p^qPBRkTHv8W*A;PHB_eJyd538_{ydjte zBqM;0q~}#UnRR5h9OP|{@P=3)uDvc4pAVSI!L`7einTW~q@#|5*vtY;9_1n=lnQX{ zxyXyjfH*5p@mqEuI&nm{Op-pND;!F}Zsaxk7?NHBYEc&F=Ra~IbkPNA9&3s)?^`od zo~1;j;eq)gdLjME>M~o4v5i)w?A(TiyH%$hO*Oi!%p#f(+grMFupNnutfF%mnXYpk zTuq&h&XwoiBvDS$zrO#Xlp$+L8zYuRzQ_MYs#{xJn1rc*;L78oqq8`EaMEEnqjVrl zI?c>NCd5-TuQFjo5^1FO7u>iARrB8NdP54q9DNQ&Z>vG_tUzq&&$A7f#CiA-Fk(EL zNJtD_-H_x*dwoKd+6HPp2?(Ls8#ZaX>1}`ZgT$`e<6S;q!E&&#frp=Y`7{xj!4`-4 zYt=&uu{EPO57n;vZL!K^7tx*ZpqCWP8D*lVC|6NJsUx_?9U zKzXwHu{n%@{(N-1b5uQR(1j4RPYW!UBq$uMq)!wG-_{3gT5@X@5#8R$I;#hQ38cX&b(+``|bgBRA*2K zjm#^8f~BMJHuRje8V2I5ByUO#_ljD^blq)_)SF(+O*@VMyeq;MM1O%=S8j`K5Et)Z zHo&2YTmByXUE@*=H_df(dEWoZCRD;p&~VmacOf_>xy{lanrrJ>7uQGQdUHE~8|}-y;BoSw znub}Y-%ALnMCU{zbkHJ=674l*I3vEHl+8nj)nKszQKL(pCSz2597zI<1aATXC|}+5 z=Y4%(sXp_@KC3fDW?LQwa9LQ9iO6dgp8z`@n5&L;HgFZ#s$aj;4Tx~a@9_!jcX~;Z zb+@7Q3q$HZ`Ih(VRgZB$a6FZP%&o~bF{f~x7bi;yr;#yfvpY^?tg7f)V{Q<EunWPZF zJd}MWR$J+klxn+kaTy6QTj=6J17dQR{}HYnnJBji$j?43udZpCz-*_ay02k1?0#~) z4mgRNwz?E^mXZsq`$@5;zE?2?VQOF^Sb#EQ&-YIxA^X3AU?!DW!=l$eWe^p#>@wo2 zVY8t(B3|DN*@_HOpX{s`Za;rxy!>-Q$(L-@G^K%RD*lW<05#&@{hmP_@7Eq zrbrO~h0M@sS@BP@r9Xe}1U#7C6!~co(kOgxGUo{$dZ|*?WrlXXT9?;WkP;$Sb30*? z3va#iJjNDiAipW4|0cjq*N*ZLA-RbDJ7WG@Nx}%!IQW}O{+?4JRF_ub5vzxq$DT`G z-4}_2$OYx&gMnzXCv6MwF!^a8gzNjDS$A85jx6?LEbCGy7Sf7aWQ$XtrK3KrWr`T` z-m8cB7&&RmM0Q*IBKxmFBC}Y9-Y52xO%?VAy(USgd3&eJ9o3TGSXWF};?HND?y)_@Xt*-KG zG+D43kHi_?>`6nV`Oidg&a11UBjdimUB(3HRd?@qNABgf6>8iD+d}D|`L>84)_Rs= zr&UF$gvit+=X_#3WTpNMy~WVgEjPimK~k5gbj=+LV<|HIdU2wfaI9JRZ6dgX?$3i`JO9A{^ECnFdqLypv=}dwR!E0QWeX;D0rlhW60f+lWN`%EYF+%tT;Nq@W<$hP18r)@`pO{W*CBxR}d5qoN%RX42LC8RsNtgKFga#>3U`*o$$8 zCsL{SiPlC&G?BDctJ3d#2!+1v&ib5 z9EZ@1nC3W%=Ls|2w!+k1Pmm+C8FB70xs5>hp&mOS=gX>`&zrEt#J;$iXR7&jD0Dfq z_9JbCxHG=WM)d;z|Jc8XC2-;F;=v(FO>lwRaC zhHdq6Df(?ViK2*Ah3*`oqsB8iEi)xZJW5rW{LLSy2rl^4R(8&1ivM$5$L0F#+w11# z%`kzz=WBU2L%aWo<4kmf_s8{D;>Wc9|5(iVhGiK;))qk~WB4xX=S)CC*FK4f%edLa z)$l0FUR6EvN`U7#Z;rm%lxXKk6qU=4?gdl>T)v*CXT=PDcVg;%CoV;ScZSWb6Q`C{ zJFQV)>fe@f#XaveGfmmIQ(04Y(A@P{^JK-xk&=%?nLy0fJpS&2O`* z8Zz{Fhk5b2QY@!QwdgP7$#S(Z=oM9AEBGMFx#fY`8o@LA?$JAYy;v?`a@!W2^K+v4 z#YMC%SEuVJPVpeVtN>xs07Kk&a3us#EF}qo`Ih4mDM=}&#;d3;>{q8H@t_u&!nQ*+ z7xL1F%f;cX37{_7K%j>&Bd zTy2T9UCmR%7Xio<@|UcgU#nJSxV}Vk`2C2-*DV`ymCp4Y{=zV{sV!w0(1MHV8d)tuud zk?^zm{qTOPCn_4r8>fTi}I>>cTS6%v|R&z{hVAa;roF}1I{cgZ>HRMZPZySEG zftz-Y?EcHgN01e{(%lnk(%g8VlW*nMtm(qa)c5mZr+#s_*M0kn_ABLww_^}1>`_P) zYg3b}r#;T-lGkoIYHPvaZl@_%kjV;Jo)n5ZD_D{vwzb^kW2@}qP^ul%ey#1YMoa8z z2M1TI!ZU(=;k}7W^tnV>$j1x;|pwm~`$t*kOTH zYM}{Ca-kb7+Gliex!NJQh6k^xe&9W(pHN;;IZ*yo9q|x-hAd$`Vq%HAO@h^Mx2ird)ja zFeXX9BHME&rt@d$U)&3$b$z!Ga$T`>FRBoyb*`d%8lW#wigwfmNvAblp8k8HLxKZO zQ=X!L(3)cb&w~+U52eH=cxnYHWQj6p|0%mg=0*F;Q?vph1k*yMpd_s3w0uI_#jP8F zR8plRlh)kW3PD)$!kP4qE92l%l+lKT@$Kx;nWV6U(Yq~rYqwGL_XOlOQ8D;XDr}kC zA}g0t;YCMw+Ztu7cF)R~n>=4@CDA>fvtWsS`hGQVS7|Vv%aGxP-kMXkr#ix1R!_%(F>sLi4Y#$Ef)K8w9+-_slwbpqV zIEX%l)9EtoEIDaimuYw*v5}?Cws4D#8_ud`up~-Ob&Q9J>q{9QhY+mQ#goozcIR2~MRXf8)yiOaV3f~r3aBT)bcpPoNLQHeSzFvHC zN+wRkgcg+}1kN`3eJeLVuDu<#R@OdDqHUZLILs2+3+c+(*IO&)c>AxU!u8=9P-G|~ zdQojFS6tJS$>)zX9iI-q7snqo+8nbnbv#u`q?5qQ&+)uukVj9^FY+X1cjWf{+nj$M zkZ&IsO4;W&$@ac%TnsohA)qtGSPT*l=(o0+ylqXUzA)t%$~+<(ab-m=F5w3#9nl&! zqrr7+o1xDhPi|&(xE!>X*E|N~3Bi^i^n5yO zJAQ4gGa4FO^AW)Mw9eS~OzW%}2l?H6^4x8a^McRzZEqpc)ElpJz}z?LTn|1)q5Jf; z8-Jf2P>$P}=9!=~i30yV!^d42wXEIib~y3=#2jcp*ZpH4vn+r7KPS;v*A>&(Cs7o7 z$B7d-fOgAoN5j#-Fd>eD4IU zg&W872J&A%S=OwH(H=Vc`{uDL3Z<``UkqH2Ioav2)#JE$8F8VAdFXX?IU1INM_Cr_R`w)-vb zH}@WJ7xi-z&pdBzP#VkQrJ~1S9L*m&KskvrI(>S59jbRu@iUZU!-anJvW{pIxUDrD zYkdH?-0XJ-z5g-soyg|Sztr_7e?9MErp|Nu!Q6F42G;xd+Cwov*x8GDPOIlUPxbAc z!TBUx`?3&@WE+f3ivfVJXy#|SD)m-}(V=v9^QlkLTL|D*wEmEcw$EdjHSgYZu;)|Z z&-U{XchriU&!<-;o!8t;jEWeC<|;7slOTPUq25 zoCY^rdUc%-lr#oCQMTKS~>rQLkazIn1D}+f;q1kS?DFdY10_Bhwm3Rg;}t7)^4YU z9d-Vjhi1JIWrtl|M`>tP%^O$Txx+$h_tXd6#Yq0g6GhLfI=>0uLA$& zUwNUnP|ZtVzIQ0&kT3Xmb#)wnj~!OGHA^^%IKCIDZifw+A2(N*ZEsPs&P6qr`xuX7 zV=@=sTioJgU%{AHo%i<6+4FiR{izSlUc-KQQO`H{<;Bacdmj!W$7L4b?V^73_pe{9 zTY}y0yZ7KVrym5x4uYP?wuImPsrQn54+tVbrp)tX)V$6%+k#_E*Zm_VSwS$6pyv|d zqx~><60aYaNDC^^J$3Qs2O_{765L-sAKoUH8Rx<7IKRefQ<#(cbK&sp7}m(R;{% zVT!Vj^S$eu->Pno+Y2ppbj&#ddOWqq(+pgO;5xyL?OU7KMbGDVwQ7yg6x!rW9!JR5 zwE4a_x;Sy z+v?2w8=D&-oNf(Q_tXv;Ef~HuS#}AXLP693Kk%s(av*wR zGTD!8F8>1C?T|G${SC`XmGgfF@l!=JUt~ubu&CTgqQFAFclUf`nQN9R(+a*=5JP}N z35v!4VAhZIw1l_B1O@lfQ|aY!XNCU z&x6L-IsQW9CE!%@T&n)GE#K<2b{KD(uIskbbqm4+@6UeR3ATggTC<0dn7jVwd7o## z+OBXY?j?;G{ z@Wegyz+uvW@8vzr$;4uppL;q7_As^$ac1j|H)11N3LdSm{W#av+xajrh>~7=l$;s)<`^M5z!fTgN`y%Si@TvW` zSZ)1!A5&>ZdmF1W@RV8rn~z}1SuDw#hqHoDqsclKffq3>|HfH!CZ{-8+EDa2cCR}c zNW5a|Ra`#1ex8tkv@}lM!0M_$lM$WVQNMkC1!zKRP6-{hzUVkDOZ>R&9q>8L-u$w) z6^ynsJI-X(KX^@`$!Fi=MI{5UXgh%a>IU9#^rMo`l9HE4d3@V3Kb=H^g_2ktHGe&C zEq1j%_3EkBT?N;@Jc;sCK9Jxeb~8*+9VLADz}r?h_tm*|%}(&Lv&YM^x#yd^e{Tar zxBGyr>GH@CYw?$-dwc&r1^)B4jkl8%y1#9(Q~Z}U2b0+#FuLduM*A_bhIV+<9}Pm& znyTt?Wvo)KJxC*2A0+lZlazNw5qdxV+->g>L`!I_#9`7Ie7Zh`b;*02=|I&l9*Ke6 zs>1QhbO!Nz1D(`VY}PD91l9$7-fyZhc%5On*L}$OZF-1Q7gTecX7Sp&hfoC2f{Egi<*G0tai~&QP?Y zZg^Ifm6cT`D{?T{oXz@!vb1i?uI3y5{#@3#(0#9Gx z>v4mDj-Gz`GvK}LJx)85&z6p4GM77~IeZwE-v+H0u2Jakf}y-TXEYA;!GbyvRsBC` zi1Yc()=|Ume1Kf@HoTwXy|LWX(lWES$ZfzoXBiEH^v&sMJKXWCeq?%WO^$|eXA@6I zuosozPLV>n?+^E91{mfQr0n=R3+|Zg{J^%fVn5p~7 z`aS3i-wMA>nr~SQ|MevIM=F;1c*FCdkiOAL6K%-Ac8rJev`2;M&2&O^x#xZABCC9I zCcC-A&?56LIGGpx`IGB0LBLwk*-72{5hj-y<I6ah3@7v?)$`hddBVnE`)7Y z^yyAukbqecbG1|)&;I(3_B4PMkJ9n6e3&!(TTyUek?Eb!Jl7XiS(+G3h9FBsR1i4F zhS=~t6w-N0m0j}A2ew^)=-UStYmMmV`{sdba<0AKDZxZWJH4}D#sD}$wr$DkGpq_` zWsf+$A&gu{=YM9Iu#HY+vj4YX`ag*5xwoC;(0`cuIP<1cyg&1kMEd7XYLL+U77p|O z;{rUo|K|mjRaI~d<}A1d;1Ju*G|w{~p3t?lC_e=GdqwSoLH7!AWo2dCX_>pjwmQ@H z?MQA8{p$8lpER(goi1J})Aa^XHebJh_q6ci1Yh8d>UkdDgg}bEcQZq;aLAHs79>7f z7(s!gEw;B+`X2|MjqZorth(_eBb$wiNuy+uQs|vatp!n5sdT?yyFY2Hs){-aqD~Z= zWqyqGV&I0_oNS|fR8!O<~QKj(;c?T z8}TGUoYhkVvE5zFCF^tBh%%+ml}HYJSXpYBYG@)i&9s znlBxv{$u`KYV{SMkU3R8L+;3YK2<~1i2x)X%dzk7GFQk{1qpCpefM73msQVrob>U! zznD6$Z6)X8iWBI3`~~KS=IHA1cn#(wT*{dpP4~s*vs~1-)M}ObSVW|UAf?@q_X89D znm%7*?{^GQvskKX(~AZHfyDE<9U4V}4_N zNmMlRg$<~+CpYtdxYYdK-ubGapa4_-IzR;H^0bC_jLD_?>pZ)=ia_*^JTQQ zJM@e-etNuKFYGg9E7l-6C;6K=rZ2wLcIZ*|Yxs&}t5s_)(pW|`d+fL69-7LdH7aDW z(RtM$eHkv(IYYbq&!5UuZ*vByUfa0>ijfyv%ZH2m)4fp_=e4_pvnsyp7P%uqfmz4V z!T{#hYgqTI0}7Vo7q%6)*SGIQauw79-RlmuK&UocFHgBydDy=z<}jU0+98quc;|C; z>oK#3zOuG9wBV}4YW~MU@Lm@mf<@h;uUyyb3A$;CR&iOP@bhoyQ9&=YC-Y0*yQ^>i znx2RicNaEM?<0xVdnpI&-LJ|F5ozjVdD9{&g_t%%N87gMU1vpOm5WrqZI$dKz%8Mp z$>Z4t!Zyf=A1w)lgof&06xCSFrV6|ouQWsvOy@yOmI-R{hhxLzZr~PDfedb*^5Z@U z6a;I>_z|5D6-B;DiPAJM%+1f&XE8t@vYUcs|_JA2cod&$)XyoSY!V z(QTf#I~o}psvVA=l8R?XpPxGv%k@2+oUhc!L{UTy8LeSq+B!Lj72aQ6#KTZkA@yX- zAta~?{hgi9n*7=(FXdJM#aw+v{D^nhd1iU8u@3eI$uXZEQ7TdL9e+w)-5%3#{#&x6 zpYe|7tNT|O9HW4S25!M)xZ<23H>THzk))kkGzH+IUh0j0K0Y6;mYO9UGj=)5FJZ~W z(cE?A)m>l}zUgqK507=Ah2O5WT*k-*+>w8`!3}sKZqN)WuF|(vT`&BuNcK_~<4|!x zQguPu#q8a}^Z!=YwvRPY+xWjDItyb25JsRmsnpfFqziaM8&dwU5-75~WnHu**fmMsJ(XBt;bQ(o7{xpM6M_@`JuxsA7B9;C7-~7Hml>NDm7Z{PA*e6e+wN zHe;)#cxA*cu82HVr&XxB$EjYo9hx0$Y^$n;5r8=&m;Gl+P9IB@^@?uHc<^m_|3ZDM z&u?=C$$?jtiiZKA8mM$GHQmEIFC8x9KNqD&Z`e^(g?yjy{T|NOKbrFN!76U`%FLLz zHsib0!$ym%ZEYCo3{{{=MC=IhM>1_C)l?~idg++xq=Jiuuo*}m2jRZ;x`G)BRRev@ z0-8GlDqSuzQj`p7Kc#_DltHG7;Yo)vW5qZ(mNPxFsyzDAE}#F;ng`cG5GmW+n-%}(Iq(k(Kc;I?8Aop%;)g2)Zs&(62y*!4%NwNZvRdw zWtepI2K8;2+ECIob~t9tddwG|8O7N%oIhwk!>0;gA-j$(->P7|$fAia=*au7K~V2{ zXFcE=->GdVQWi$ex)_$`Pl7BNM3HQNNKV#e!WNSdE$QY4@8*x>-|?NIpCwtiY)UVm z*1sd?1C=5v9_kCNSviLxEs^qchC?G{KT_;QOsqr6kVnntMxRq{UcyOnj@_?A$mE8a zz1dtzQDol|a`tVwQlg_+B&jH>kYf9N3=&x0qC)y!VG_+ZhlrH5Yv+@(FZ!glpm^Vu zZyi^VC!L4%ENX&5DWd!?$dIDHyWhLfmw^~J4Hm~aDz3ud?WBovQ@cfEODxKXeT!QNV+HYI zg#$&RWgL`XBhcz>9Zr}I^H4y8{lFZv7qZLYLq4$NIHlO1z#K!u`AZ-box1vGJ{Iju zO$?W#`9w~hvs#hzarudooqvjXovRtDnI)E| z5@o@H`G#QRk3SzC>o(j1q12Z`_1HD5MlfBoXfD&61kcbaBOWD)R<(hEOQ z#8J(PV0Wc+&dPe9gov3%7o;kcz%dh2O4^_mdak~(X8)>0TwZEihdiYXd_KWt*PtjG zIL85=u?j;0GuOl}1CC6k0+ansA7O$Y;`!N!LLOW*&@L@x6d7uAjMS)P638GOxnv-2&9b_-(N~1RD%MS@I6E9d zT(fBMS*m)(1Onc%`w)XxmfjATrsX$gFsnsYynN~=ayl5if-{$CgzcZ9r#Ez}?Da?p|pJ{#wEKnx1m;M~;Y`Qm!;)${$Czfq~)u0xX zpA8oqbg}-gaQ*&z+SJLIVnw4I>$HTUx>0FP*`cRg#Z|@(kCz&KU&=LmU?3;X9NWnpPcds@jEoJ3@Uh+oF8YxyXC@qh_hQ3+$;#8~P z>pCT-fzv{CYQ0r;Z-H&Q#gLv^u(*R;#Tz-f5v5|4#M;uil|vPHXH~8urGij>T4rn~ zxgZKG8e7pH2v+=sfE%wQ#+u!Km`zD(!hp+t@C zPd&0Xs!HC7t2X}1F8rIQXTd%(>oY#<+kan*l|Ipo(Gdw#^@ia|mbH)dM^iZtWDUf8 zCKXj|gbx~$G00<(QN!cr<0VrpidJ&!lc8FR#SHn;BK_&!@HU)SG$ET_yaE2Dxcd8@ zkp}kO0_{@Xw*9p4q1l7vN7$wu_^vJbIz>uleeyyhRSS|0ZJ%nkq1w-h!|_lf}q1 zB9|(NHB{jf`wD*s1=pB5CMNb~Yrg0FXg;k-nNGkdWymPRvI+eZ+Ohs>dq<*pD;!eq zz!|n_?oG`o0LNDvlm*xLxy|XqV>&uD=1*PdG^QsPzz1@7?}vB(t>O%xc@w|Voj>eK z8a7(c(n{iR!YE^sLoXS>_^sh_)`h=x+m_Bzq}s*ixjrSt<8qFzGn(oXn|+b2XwG%G zF%v?X;K6=SQRhpFU8mP3rbvk(njpxxP5B{DBCir59zJ}4I9wq#G?qRBpOLK;Em;t+ zRP7xtSzsSQ-KX!bHV?z?w~vBild;2v^Zrou7pgRHtcr&?BnSL6=r(!(?Vs`Ara>1v zcdmClYC$UN>ML$RONhpc%o+@lp|`t`?6K~Nos8_yG}pSP+*I=TaP8HRLcIoT0#l7{ zSNlV?jKzf-`BV_&5%{zDsWb{o!H0uzSt|Anuc|ObW2Vfpf@r(Tu*%AF291`$H_RL> z!R7gX%PvA0bk6t+O?MzK0yV!Kc1|W}-ws^N+x)K6e}o>fnht$%dfFU~-&=(QNQj-@ zjrfOoR&kxRxn%r1%>CMEb|$*#xG#nQdGs|d%`=}cgP`4HRQ@dEddo0~*m+hk)4QN- zhU8C6M8PO-kE|A?#BrJXH)V3$u&|&SleVJYWL&X;ITCV1BkO>9RB<95S z_m3oaq1p#wN=5Iqahgv`_CEfSz+v$hq3SA`)@L!wC>l;K^_!t`%8Z0z*O86PNQ9f zqf8JqsWI|L?|-yo<)6?T{7KA^&-ZiYDZW@nYzpZ&SgUz&x%0Ob6|yK5X)mgKoa}H^s}GCTMUR=}{Hnu+b9XkF zQWa9q3l3VQlD5taU8bkOwwtRn?O5`M(QL#Qj}59}#gjEK*a1-ErwsVyPw>&JkJ(=o zODnl9%v8ybGtRD+O8W|{a0^ssQ+PRxVgJ_M?G4?!(VFhyanHiEb1~7`2hJ zElWW~X-mcA?`mwtZaFBVImltZCW0uzr2TEFRgo|(u0HD5+I<=}=D&YPO}3~GxzY3! zpYT!ba`77$e8l+WM(ijY8MYWump1XS>(gt@r$1zUX7T)!=;}5AnpnognRi;WYkRDe z+^!+@=nX^H(+m>i4U=|9p|{`m>;vjg4y(RpiHIuGLA1fsEUr#TNy*$VU5#SmJ;%d! zp=WVNTCEI_dFRLe1=rAHAtyg|&SG}64Gc}qRMxkzD5&#)n+`ht3XaL-+CDicG{bW zwOlJ?$7jl67Ib;!CuO>5{XHzAXLxeAzd*~a$HVbgAnP@l(2tTPRV7m#V2ufw zbxHYXE@8vD#xGd`ul^nn1fK-@y?(y+AS^tbt{?GxrRgZhtsg)VS{FyxA1cUVxPO#} zTpkF$YVSU=KDo<_e}%UY%$i*}*lf4?)6f+G!F_A=E{;cMq`kjCn&2DPnH6aryXt)(HhD&u3!0Z> zr9FCoURa{1+m??lC}9|1(ANEBRz1$$?=XdfAk8;{vzfgHaeYHWVj1b5RNUSewGan} z@^$n}4Z$PDr^VRG;$sqJNZEL>eHlYiVp#8}*>)j3Nc%Kg@9X^18aw7^m<;=N6-yEu z%iEaGwAB>GxwflQj^}m-+PUwP=>?Zw%B1ntK+EAIW`5h!hkNd6?%WS zY@IqsBD== z8~^xXu4BC_ss!s~+O+Y|n!0hL>7E`uNSf$nhsX}=2pXNv$e>x8cy>km;L_;uWHLLH zNljK-N-oPR_cWn5CXQ&rDz|bkH91!zh20}2DbCM)#9>-tgkq_Rao7mSf6PSfP}|Ml zw+hOftw1OFNrsA!Hz6PgTR~ymX5zseQsX%I$AiroZwD`#@bW46*0Bi6*G-N%8mf2H z09W6z{XMKz)T}W*ZsV1VI8yc(d3AL*IW+v3b2ZJ#2)NP&Q+K(lzfZ8A( ze8r*PUl`z5em1F@DAh(Nt%@L!bZbr}EL6^gwbZtd`B+{_Vf2$#DtX}tqG;h~T{cm? zco_G1_&WxynPSrm%B;$AlWtdE6q{b`O~dPUpbySg zT|vf;jnD_HEyl3z+;-rzqi1Hg6Bslu#5Q?(cq<*}t@32Po!CgYu__D^c;5KDDV!@V+plNx-FB7w)N17s0Ng?F7q2640p;r#|; zWl`BtgT*hd5VL*iw}b zzmc}T?-Q8-En0rJL4w7#zk;b`DyjgC9Zu}yk@3bUHA%GD^rfM;$z0!QeRUH-hBq&c z56{^f+pT@qM(4dxP%do=s#sRD4P8vE*lU-mX^KrvZVLJj)(_f-gKliD|K0*d?Z)j; zq&2-Ms=C_2$b(~+?ff)Hy=}qo-=P&DuSb}tH<4G7qri8CLf!5at9$!PSgicv&At=% zu(uP7c2hs(Cx3+D&y82svHVUX0rT+6cm&$3oU(K7Mo;Hvd_?A&V4E+IUt4l($QLX0 zn2@uf6m-6qnm;rYS*lCQSjs4}i|Tq>$0LwJHDsWP6zQ53Gp1f5LmF3k*t>V(v1 zxR*_Ad`2VW{~r1$aN}I5GUo7K$rqFabixke{!aj@$IpJx_K(=eq4+0@NrP>IZAUn9 zSzWj*UQEXncS0Aey(*R`h^L&FP=Xp*H!PO<#6ft#Tq5w)^I^GR=l(k!O>YR zhHW18@^)0?^ljlVb2_0sFoUrD5~1R|5)%zN|Kb3ogOM( zIXpv)UC%eenp4In=Y%;jVl@+QM>d?<`4&g^bPJ<-Fpf;od}R|oc%A?LVa@8-Gp#^> zS!H;rTI0kQCp>t)I^V-F!DG)Lh?cDxVeNv<#>ehlEoHS^{Yf0I%nYx5k|tO#AyA4w zYs^HLO^+o-5-F5yHIj6JFinyzckH|?gd5#EzdnT;)V35-nxg{KX%9`a9J(*aY(r=$ z@w}FZ#i(q%WzvMvt<@t^ z-4xM}CP}~ve9Y?(;`=a{mU4g5V|-ubL=c|Jk&}~LG2X|cJ|MQ2u=&?G$6pvW9=|~$ z;x*?gOf-!Md-Qy<340Os#qu2_P-1q?>IZn-Xu7f0boz%&SrM+1wcA>OZmp(%=97?D zcwce#rRZWrf@HENSQW*)BSI(2)H0|Z?h7~_DDX$^UVd#NMf5vxW$Jv;Hl1d+Wu-u* z?qyI|*Z)jw@X3sN0^YsZ>}G9WvADjN20Z+JdXo+A3tE$M9N4RD$y7U>Gmg8Kka}!{TK=) z$rzTD;e@u2^(Z3!l(GE`LRU*~N2f%!1KeR2Nw%*ZJ&#g)rX45Ys*#;rPse+5rI z5`k9FZM1Bqk@fkj-^1flGvn*kM~o}4+Y}ip=|1XQqo|nl5O-SYREJ9%i}PVJ#M*X) z_pzxlauy<(a=h-Tgw+Y3RcWx_H%&8kKERx!I91Kc$cP}D#Uq#h%e6QhCPl23iJ^F> zzsCADJdSDeyCzfJjt{QZh{uBa8<|6vuvv?(kwr1W{pz?2o0>u{U($QrJ_ELa1lumT z;#KfX@sp(n|CwySou{47!U8V;RZnDuG+>~mO{eqkS`~ybek2B`pj@h>j-B);_>3O( zeG`RY?tCCd->sm>MyALt2{yM?{Q{`1Oba}&AtPW?`c@_2^7@eH5Z%en?T9KiBqiF` zCH_qZ6Jl*nNiaPZ`8q?#h=BY4%!l?+8}Ih`yat5N@lurd;mCZYjc?6q5Id`k@+ULD z6f?A3E%@|`mkHvormrvIo-KV#uJ2YNJUIB#l}{aZ+IGrPGa>MZ9X?z zo<9v)erPw5&d~6M!?%pZ9X)xKK{+>`##g<(tsfrk=P4ySP1v#&el&3)`Y|fd;=Hwg zoVI_0o}pYqSy#Ye=^5@q4YE{NRIzxu3SSl`F<)e16evlBiN`AchEBVC6F)$ka&9i> zdKQUauC{GHKHc0sU1ju!zImz=xVrb_Yx6JiBb{lzvtE7p?ct(*Z`}3Zb@TG>>{1We z@7Um!Ai{O&>FN18%UpCgo4yIcj* zXSg~vhr5U(zMFNp89Kj(fq&@ezz&41UqsP5HdG&=Cmv2+{%Cc);(a!-!R zn+2t97DqS+!I+CEL2PL1|{!hlJP)dj#^n;lhzEJsm)Y1Lu8G#x`dQfR9 znLYFKc=G;&!Z!nxC!5^f$SM5{K$f;=(0mT~E`c0}2rHlJwQF~#Z+SeT zC}-?lUENkTcZ8d2nc4MbAgxcU)X(%BpkLU%_H96RzaHBCQWPhhZh8h~UQ2OBfgGO{ zihuY(3IpTYy%cUZ5Z4b$ML{D2TJ0+NpO~LN;Xl~eAa-<+<>ck4Z#C&SW|);|v_)4} zb1b=;zlGr8;gx9KutOY$GH!+Pv`eb0FK_*mDo`#t!a z-y;Y51%|5H@E>PTGfnGq#6(96OG(jrKQ?427f-|dt(7&0bar+!cC;E_9TD>X4($rQ z%pB+WeH@${5WHNio}p5_d7LE>pp9;H+O!Vy^}0*pSGN8sW1Bs-5JW_@tKRkO4TM_G zpssgtFjhPqpWk-=g|G|v>%)(_|M;v|r(01?O?SfZuUaWrocHH?-AP9D4x2yTH+!KN z`;U-IVxkgEtj;%NfF>y^M?yvQ{$&Rp)e)=NMJ-SpO#EhzK$U&^pI(57=N9^b)4a3X zh6@oEhBBUsfBaZwy$oZufLOJgw!HUnMfJH<9ft!S6^lBeYjF|PhhDX8Q=>KC7*D;{ zx8!%Hj~lj-{UvvwFY8OjJHHw%7Hd=f5=?bi z8t3PFHmTmUb$BKTT4H8+o)9VKdykawB?>qt^0j9Q zKg?9vqeIe`wvT5HUcbm%5v#||x`Pv(jv?8yIr)b(@=|Bz@2bAVgFe?YQ}LI~lU8*q zcHa5dGhvayNPN_pDd_3OV!Ot@wvJVx+J*;7dH*m{UttZiqSZ>c;elA9UNcddI%^@$ zl(@cS^w3ff{bfugva7RFD5<%bpX6)s<-t7cZ|^6v5%`x_cz7S<>Ea8tnzr<7K4Jv) zk_Q?7n7NDXRSPot_wOG}!Kz}smDYXI$(VuHa&^q3XA|6Sy-hbO=&RNJhqD!|#GPIB zA6HauaP59Cr9a8Yz-C%4&Oy3+de=8LzDLS)dUzDs?oOE2TFna^mj|~YJiSWE zpRVlcNnR%HW|=N=Mwc2{i6#8_hi|LXSwuMWDlR6o0&Y%$sc^-RG`+1|25 zxu#PC9B{UU&%!2|~jC(sc8`zj-PuY%QMj$y-~&%Q-%Y<=cPARlqS>eEqu4 zwc@52GIY0va(l6-Ok;@=KyAA^C!1Pr2c{2QJaVo=h(e(lQzAJ)M{V|etfPZ!+G{>$ zNYt_YF1gBQ?`V6+t7Lq)hL7|L5M1rAzurHnx z$H&*ytL=2O)b`h`rg^MFfX~^2$;Mg-BOssL?XZ*lq0-9`W@}*iKw}Lp&hIm!*r*o9 ziw^jZXTbMc_xD24;_E|zci(>scm|xPn5#^Pe!Bi*^+0f( zA^ku7O5u(3%g4RLg^;zi+~V$q@LabVIzaB>NTik37gHj3+RFvFvqn*b=x|x;_Y7Pl z$>i~mCIUk2-1JDHE8nbQLLMUknt^!r&Iu}q%`TWMx*cwVjr?*rg)>Vo4Zpsr!R}_) z(a%o=vE}thQk@Xsu9%uR^;keL6P6wr7+B?eLQ{ z2kLM($G?SiMz#9>40~rzn!V-C(eWLeX|PAP4tIlQopG2?1uWN=KzAqih91x9fy-P` zuj^q{=&EYX+uX(Yej6d^ZkrWrw$(Fnl7pF99-qsqJBgLtWF}KPYr=9_2SKgz+&hej zPa&Y`^V)Z~qK_s!r!@=V;n}WLR1E6J@yfrK_a;@J;_)zPF<4EK=<(FPiE_Gn%rPi9d`q%|)-NP+vu`9^b$!v~PsQB;mCkM-`jD~M}=l(c3si&r<9@A+3 z%o0WTS0^tIi69X&O{S)#j4yBf*=v5lt5}u5OfDX#|MV!TvWNG-Lq0twL+%j%>Hh}& zJXFk+Bg%`0HQ+`^MM*;@iZxJtvLAhiQU$n{U3JXQ*v9i@AemNaKM4gfKYVq00i+e0 zxW>i$*MGpBs}qy(dIX$#GGn;hld6{{q`a;}L__;_xz8*qD+@=;$VlXKbB22|-24)9 zeXT@JN*VEwvT7}1wU$+p zuKxwN)aM8Y2}J*?-XbGtL0PtfA-Sj(VFOa0ece1WODF*h#{EX2@! zXQiz-ylWVRRhC$;Imo_T6fb1=FvnvwoGSJ8Ydz)6)&66eKZ29(^i6%< z+7DRXh8q|dM8(7mWOyGMPwqNGsx4!Q=GvZG(?o+KKj%tOG6ke=01#X0GUg0v=5sYm zzc8&)M!^;mc>eT?xUMT%1ZtRE%Tf-#w|<Qx^$i~#-OqvolfD}a()>D&3cZ(0#9YWe+uLtP zQjy+qat>q8H4JLjN3|HUJMDk^$#L>h_rueY#X^10DalX+0X}4Qrv-|#g*s~8 znzBJvm;yT;EkI7w+tjO;tc1&l?=Cbtri9{F)y+>?5`Za;FnTDb0QtrKE2AZ(PU9>!j4fDUV~;tN(w#1 zKRAp66nBS_+=ygDkInfI0y%qK4aPgVk~bgN^6y`A$c7$T|Ranc5FV z5R18UzV$z?FL_7^^W`%(pB}AAMW3v7({J6}HN0IuE~zH|{%zBTr{@~=-y%*U-+3~` zQNOqL%vpebzuBm=h9q&2SuP*je2jdv`KFSPf6+2EA#(HMDf;o^tmNK3{s*dI*h^9KN(9FeiCsOtjB_S#) z>BWbxpbMCKu-s9(NU5_oAAf0SbEf4^9)MF{ZEFFsT7!N&Hnz%9&AA5q$Xp^|dnNu_ z3qp?<(Hsnmm&c{=^iqpEzY}O=Q;?c`%e0$1en(?{l9ui*_ayKjz!||wkS|s##dp}< zkO0N)0iOd{?0~*A)j~odm)!9Kr}*TUCcN03R$AWqc;&T>n=^8G?T!gv;^<_R1TF{# zAg-AJ6jxreCj({jOs}`HZsnP=OTSy|5rX-fD=k5%a!kc?jD4Ve)aicYEjC}9c5So- z8$rYu5gQx(zku996 z3Zmn2A&~v2RYFFFftkay``VTqxs>UpKzkzJ^1~Tme*P!2#pwGlWV^Z=Eilz$wi%hA zNXlh(AMSK8_qX-Iyn!Ae(?X4Ts!T1S+*-5`Y3lL8o!y|%XMYyi#yib@54yiUbbT`G z&L8@QnVf2i$;zti{&o+4uGVrG%**S|MA?AluuS8~vXZZ@-12v=X;+l-^s1GF^xF#! z)klA*GT#~h|Vksp>VkiJL97FMqEtw@* z=54t6xtoAnOZtWkdrh&x!_6j%=RGBiZ$2_dxq9i^gLCg#t!y#ukQUpg7%>NjR}m2r z+}txzK6wQNOtTt%fM$cs=y`bXYSb+oAjKfwitFheDl}H}(Zvt!Hh{eT98xZY-mmS= zM+F3kIq(Map9y>t>jsL;BOT$L``rWEE?XztcRW0l%TP`;PEL;^h2jMC$zo;U;z?{*qKqIJs==#) zX?IZa+_z*!T66UqgG|~pjh`j-1(gL zx7s_}GTgKpC?xe}E909_#O2K^G}t2T3A}AzX^8%N{ zNbika7>}l}UoCrejD3+ADmFcSJbHMstgOe`*3#u(P60G@4wQZF_v)K>8Ld|A}3tw<6 zfd3i037_uo>oeG*wkK{Hdb(@#eUX8((e9fThJQf?!DZGqI@>CRfRN5=`fnaM_B~d3 zPwz5a@Ly`_7#PG+&UktfRJu=Fi!C&F=Y^a9-68xQ9=;w-k|5}qmhtf}-HGU>+N15D z%Y?bc#QdlKh+};*xy7J&vns4TRZ73Mk67+qAEwNfXtZ!I>yA!Vo03EH?A>l70R_NU zR+bB>j5ItF`K$~2QphzlgB@5WuDJj$$ZohRGsxzT5Sa;R!25?tcDErhv#HZu;jXQr zjJ9pxd=syy2faM+bEJ)ZGKb^6k<^AW#!;O%FaN@Eqp@EfHNe5oOe-P(`SEIsXmG^W zNA2^jx`}cfTwpH)k?!=MnHgs0bpAcaatvogf*x@d>r`I=OlMTDHXN8p)@*uX<|+sh zFq+rmGy9ln58WAeM%-*M|GT?6J==8kjBo^{5!wzN4*wzecj(p}f+O*`ZR!o@Iz+wM z&CXwdTiBd@Pjqu1+CV`~t&zT5cLaHUw1@Rszy?LAFt^Ep(;)^%MyEZv%g;Yc3rdjb zIY*;abZIcS5;oV@v64Ww0d6T56e7>XOMlE}!f($Rr_rODmtU)l1mFPn7^%eSIq zVt!hu)!o;IgCEEPN4s4R4}thieESoLGW2Z%?K{W4i48DD0|Gy0+S{~`$VvO(IubXA zJyl>0C0%erVyPp%c6m8WzD5F=is8i$>(kvW=I1Z^cgG>Yb>w9*1_-L4n1}-GC=UWV z)ay)uxov@MIbGLG9pCok$?F;b%jlR?*!F{Y=j}0j2mpK#Pyt#jw}ygGDx41N=9+Kq zS{+dZyj%{(3qS=0W7%Edv3>5BPsIjp5L zJz$?xyrxH|DD(=jh>`Yw59^Ujxs%e&g333^$;lQEo_rA56kx`Dgh$=A-8@wrNj<51 zahu`2C6w?uR(+*bx#8Z2l$BWp11{1F0V}&Qfb#F*Nnm#&g@G#~rxx`6PgwGC`>Fp( zI$Wl>p#H8@t{;;Y@4uQo0&YG&xZ{z4E10)Dc5lg`^0wdL@$mto>Ls9WXWh|(gSCan@@Os>3t}Ca@v41VXLxI-;vtpCGtBK` z7X!rD%Y&xU0V!Eo{kiI^dJMZ5I0UDd^z@EHfjh{|?Hw~{k<_+J%Y5gf!z4SmLqLZW zDCWO^xVzdq&1EO#t6i>?tvi}|=sWOgBjD;}=xDW=74aK^U-5Pz;Z1{keC(Y)ohNbK zZFZYfunN-IxI3x}et}MSpOTZ4gV1Uu(ht~|@4&1en07(NT;t+ffBix{BK^MIr;TeS zUp}R~xV+lq7u)k7t}^oM0KK1QR&$#`?4HlX%WpT!6^z=^(LoH@=*wxvj=7dlKX-__nkV#ZtAR zwx6`ex+dVU>d}Cv|H{4RDxk~5|AZ5$$3qVf zM_N4*j<3*vpT~^Cr&ZAv>%Fx86Yhy(Y;0Vu&Zu4rpEd&Hb0u`0G^|nkYtk&~X>oce zU4Y0(DxU6iJ?6&a9^#|ab>t}9Dpyg)^2xSlaY=@ET3h)tF(^M{&UNOx<$f$##|U}GEX zL$c1lpee1k)!?yt+twlIh-s-Vv^a|<#HeFMQR0}4B7cx`3(OyqnaG)aa^A7pq4SnRdA(*eCi z)B(=Y_vM0GzqtTq2e<&Rh7KM!l4N6|zs@kxIO#pMB=_ZL$nC|X{ngDw@5XKKNYZL;dA{uO=#kol{(Zw9w#k z!sZW;KFVY0^t|#mps9&K$D1?dz}v#0!%w38O4v?F^GHlwT{bcz!q-DY$K?^gQ#~Z4 zkTBvD0YMAn{0FWT`;nl2B7HeYInQl3=YiBz&4X9q+=(sYKGa=O>Uh-~ca zjN>Wq`-zuzTDEWpt7Uoe^y!YO}blBNPXus8YZ+m;LR-{xYb}+}u=s3d2%*KWc zYR_k;ikCMCl%F<&8=a!lo_kZ4USF%@;_1D&dT>+NZWd20ma5;_31(tqVKH{ml?~sfd0T^O&r;i&F5~A7cg6C7AFV6qP9ds$9@dzMA#!nYoHc{VR z=NZ0ohnBTKN_4K;g3IP7C*uQAyeYurXf<7({*z|p_PD0mNF6^qJQVMOzktj%8`7Ja znr@@QUmgj?Sqs=NHZ2~h7OSB0I-Y`E6c?Z6np#T@ENpBU3kyQP=4eE)Bi4$7vHyM6 zq89n`*%X?q&`-_21kHWJY;d*G+uVyDTIHW5xegzx3C->^s0Kyu`2rHkzyLdu**Nm#_V&F&7jrS_ioTxEL@+6HfJ8TR?Sf1$@yc_X7XfX%3z{*U zqus)rlb81z1LG5@+dYBc;)97tr(vO5s}0B|*UXnY#W>F*jv z8?bz8(GaGJ9c%Np9$X(uD?t}QCwTu|jXhrhkZ(?hbAuyV?kA_G20M|<&<48m#{+X~ z>)wRnJ;0t5Q_H7sH63}aobOa!JDT)9sW8B2DpCs#Ud{qS7_8|ZxROaiu)Mm{(Ez>x zzauD%i;XvkXESY27y$58yIvT+xALxC1qqfhleksJ8m-%(lY?fx3I8q%6Qhc z^}j8Op;pt-O*kO6ybip}21_!gtgpj?Rr3|e-14pk8c>}gOM{vKb40db$KwxwPWN08 z>XLUS{XYml|9#2%|Lq42ZBRE9f-ez$pl5u*E*|@>CkX-tnid(F)-hTQ@Hfc z@@jfMOAEVreFr>23lQ8wH^e$bR^+MEnG>*7qMj zz>DAML4lTDA7(6@Z%+yW008nfP&o?VUsMwy0rLp_KE$YM2JUr$1Rx~E1b?1T_QE|? z{Cwj^o~r#21Wld>=I_!JRRLQy_sd5^q|0Zf$aIZcD$&XK;a1k8$!~(}KfM4P4_I>P z{KMeVATK$m8v>GtDptUDcC@qBk`IsFC}6UhvCWdc@)V^KHT}Yg4ryNNH>2KA%lGc? zZrH!ST*reS3nuAeVinViO27hnn9t?fM6LIFZ-DChu~dcKVf#Z6D#43(l6hw}Mj*4e z!J`q%0X65j)Kk?NCa0vFJiP}f>De>#94bKoY@RA(=QMk-*BOA0$m>k(^IWlkZ$VY| zl+nV69=YQN$Ne}wM1lEE81n^L!XIt1@n23gMiN8LY9!#1^7l-~*Fwd?9FVPOg zXhOsf`A+$+lC<)>?mPFbD~Xspc3BLK?ehozQGT|oboUW=>wj12JkSN&tD|Mrh!%$( zFMpvYcm0Q^I z<)4OqV7dy`ONo_>C-wXOj%l`HmZ%!}r|_jUo#&-$DFeE{Cyuu2)2HjF{8F&sI-#B+ zU(4M%cc^+tCu@>wchY`;MiT5xi&dmjECFR|IpJ~q`I`LCzp@tg$46+q9Xhee&Es9h zO(xjHLD|`$`+Ef;zsq&uw8Z!OsC0LjyUV>o`2``p31eU|@y>cbl70cFyU|DXmS7M4 z$m6!Zm%BxgBwW1I;%+ms=l*_B4^mQEiumf)I~EqiHhY*7ttvl}KxEr1D_CD&Va+z; zpyEB=%^848t47B|MVH(Qw1<87rn~-{VexpaVH3^O!L;wSlflCIn=F+e85zkF?^(}Z z!to2BW7s^xIxdyH^npIYx!n=t;5eob3m{P=fI8%lpl%3TBNac;aGA(pDmq<8v@=sd1eq=mrVa4~ zJ953azOz}Y$KBfc$ZflZtx+uJoW+%d*Z+_xEDLbEHtVQ9)!pC z9s=sSJb;C}yZ^+1)bOv1g4g-59h^kS*O=kyUL`1deSJ+Qa1X8rX&A8dm=$lP59Q4buCJ*)FOl-{@&HPVrd1S#BPAs@H_0HMr92Un+wY9mn^V@Pj8sxHGTI&u`1fKUew%T0#cp-;v8nEsw zes!gz%f;1Iu~A79;vt{ryDv{};5~4?1O1rF@A~6QcF@_LABo9vIu_*p`}YtA&F#(s zWp6vib6$I>*Kj$CS7uH)a9{igM1ihXZs3N6bEXRHNmyLIVE*7B^NKeOO-RTak|%c) z2DZ%kW@a{UZW?RXbCBq%#rWgR=J`$=7^Q!PhOm7=j{v!g|3V$4{vN+n-md{*||mR$3Rr>0c8dlt!# zJ3oqumc#vR)iP&bU`WSrlQI+o_0IK8Vh^F9N7za+8BoY#-6c&F6k;mqW&M*bS}dz{89&h40ZB?qqM4?U7;(HnM>GI2s$mxywWUlo7-Li_ z=8m99W0dTMCLZ+H>!S(hjpJy2V207E1t!UfAL5ZEy>_x$(+3m$a|Jrf_&|klZh={=0IC^A%1N^h`LI=RXEdG!UmY;o9`3Clq&mTwVx4_cTY| zbY@$Cfsx@l^Wtf=*yM}7g{3qWh)9{4(n^I2k_`!wR8BrpI0q+ZXU4}XFt70VKQLRx zO9iHB&3foClbycnVzVwtrB({XrX6aMbIjP8Q6Vv)bWojk++SJ`k;{i8Wjm$P@w#Db zZ8feKJxrbkCLM^*a+PMi&Ff0{%>TjLTZdH{e(Ay+3lS9*6%YYYX{1w1K|n%~ZctLX zq*JAlkZuH|OIjKM=@5|a?v4#?V1H}RZ|0jgGjpBmI%m#5{IfUje&c!8y6=14YrQ;k zJ^tjNrHP!DcF*Dk61Z~sHk zaEqLSseu)9^V&PEzcdYN4t!Wey*~X0L@PQX5|iPTLZ8prc)EIeO_6!LM`mrTI=VV5 zW98HxQA|qBhZZj37O+$sN3_-#@I#t&hxQNu7qCt>{cv zC$Vrb2^DB{zyEt*-$*~;-s|4?f8+UPI%XcX4F7N=NO)FC{b<*_Z_NDfw&nug-0bF` z;-Tm5!=rRu`C_g-CfA_SO3BXtMK-lX_)sF+{bt99mA;R6={0r^_$AjkH8ZvAfLX*p zf@KN)bOK3y=P~(F_~llQl*f4sp7!bH zZcZOT*UmDLV-zFlE~#_(XFk$rdQVex-%JL9inp@i9GfZyp8tf!O# zDrj@bkctl(&?V!szDhuCaJIf$8FZIkS=Y?$cZ!H09sdTz#r`aL=Z43RFslSFnE!9S zW>}4LnU0YWl)Us+ep(7Z!rMOJ?aCO)O90YpTpaJrt*`sblM#yan)4s2b{f8X;7J-m3tLO8 zX7nfbuV5PbGWuzSDOOfiU`>HrxBYBh%2F;8=W8 zF0VjgU}0f#)ENPqlbf5nKlm$*zln9HIIG>h?t4at&DNxAZmz_VS)BC5Zi&DYN_*?VC72CwheZZapv zrKMfQY(p0>>M5E>j~-#RkJCZ+b@D@s8fSZ^A4F4^G%G)RvwX)E$%y(~U!PlwzClte zHpcmsm~*{xh&1sC@9Vpm3-=>$3022jw^b3PQt-lWK!{rt3SGVUq)qcG#r2RU_7tAX z&qK%mFMV)?EqpYk@#??-h3}3NSia*JGmqWr9eaY?@Bh~OvFc0IZH9Q_qgg}8ao?yo zJV9;=w%TZqu%;Cj(ftC%=N%YdpKw%#_{pHz<>AIpBv9gxB}VUg#1%HL^<1f7q&3!d zGk8LKGM^zw`Due~m*{5GRGMgDHoN&gW6qGNUd`COXZEJTZJ~pg@9!1w?(8g_9zUGS zmk;p1G3h!nQ_VS)uentJQFu7KB)!TeYKS!GC^!@j?a~u}5z|rYxLL4g_Zk5;&};Sk zOaf_AF?}y5f_~{nQ7eBE-c1-6bLYc?T^Sum_zOKQ4y|L>5yXP$xSSRvH=hq5Jp2+E zn1mX)b|B?$D$;jnHt1|Ml-LC30l8ieRTc<2WOajtMF(QGBa2NlA&e08B@8NjsDV?9 z8FQ9lo-i6Z{@dg+ERgSM=~&Fh^FA^RNTmgTjf&e1qX(NM#v|dnEudCZerR(kkJ$T= zpcL17em41GV^r1x4eEYGQ#Z*rd!ka@(J*%(8!MuOUI(fN`w z^sY9V)5p`(xq3|@xIA0IDgT7iVlvWX3;M%FMm zOJe+T{+>Bc4MsC~_I9G|nr()bT$gEjds&>uIL}Lu3m1@^dnwMr=i$XU5($+zh&f%Z z!|hf+bar&?2foB+*!AvU^#I0`dE0FykgHFXPjz9qmS`ZEjMGT~vNkov9P>S(c?7^# zYEUk(c&f7D>sMrYE;>f?0?MC=N6D4NUu0n^L;wg1hkPKPSNZsGNxM>AowQ4{rxYsIalI@z||DxriO~MyJk}{u*J_-FRO6#Dg^@ zxy&~dP<;5a`dJzQ1m~T()+KUsZ5J1pk!r^T)(mFb)hN@EN?|{X-`A8OnQ0yIc{#}T z*|>i;8~z#5-TfI#$By}PRUbA(6obdm$}8)`g(MyVHC0P=1hb4gRGlj;)v7jvj-q;wDQ!2L2@I2v*+VGykNN? z!HoHJ0Eal+$eF(1bw*S993Iy4eD3F;LWh^S&isF@y;$G1R5s@ps+PuFk(Wl5w9c&X z=9IffmQk{yKrH}CC*)>#`fK$H%b7alDW&-vh7A-<@j?y6_MrZJVVFkaHql z+M?`Av_=5267epk@{MU08+`7Qx0@=7ET7Ci?JQhs@x3Md=lx%WoW5%2 z_CYgVacOBPzDJ=D9p6ILB4<5KQkjU&?h&-l$mEslh@e_FJH*%P*`5}5nutgl7%b{` zsH;Z2abuKk=FD*+BS_-$BYpVfiNdktkuOU{yB0?2hAO52S|>g9bkF+Ci3oAiLa}#D zE30p=AQ1nuNNCkZlRx!107nN4(2|bN$_#_6heq*EET72AmRoHO#%%Ie%JntJ2DeDT z8@<R1Jf8ggAL<#Z6gKVNo$58n&X0A0rB28yCT)Z=$ z!{_{>cWK&i=hEB86G4Q-P?i8JkIwT|UKbZ>8_J{Xq7y2y8YhdrN$0KUHRs4p z{uB8cCq+z0o7^k}K=Ga$kKZlS^QZ$pzXMs(>|AXNKy+3ga&ezS&C)umL;5gBGm1{~ zA(z>x1Z)PQvBS^tLQXB23KbUk+3Sa2T1SU+Rdx0IY&(bj#(!F|4Y42wvUz?OB9$nc z*$hvw2KzRRS6|g_acTN=c}nm=!)CUvN2={l^2KB0Lv}jFB152&aQaPyW$eH?zpEBE zBcu|Z`K7;}5`)aS7r7A~dryuk-e$D#Jv)PDWyoNjy3k>~S6J9%ZTEx&jZYG1vyy^d z4qBp>=;>5@o{Zf76#$Ij{73;n+uHi@@#D{+?SY{GN?d%#&6LTQ)pNi_>6uxG|Wac(P||GP~1|9&Zbew#0Mb&+;4x4Ie`xvA`fcx^QMp{);v=TUG7%l}=7_kV&G-5WJ=JADf3j&VHhb7Y5x zhj)sR%g}9Ka^D!WjeLsw3{^7TN3dVgr|Ei@NSK zn-Nyl@GJg;|58!yzQ#&By; zy4sZ)0Hi&prKRm*oojJVZi*ho=(^v)zaP zytqz&o;=A3H4np}V%QwezH$^3jLPVWEAK5e5vwsn9g;fCIN2!jmRQ|@uWets-*5z{$N`XDLj zjQ7nC-cDhQBxC4$HG#Nx_zBIyC+gG|uug}s9ocO%}`u)>GEHDd1VBZ_GN-ma4f zc$*C6uSHb2@k{w-z)9w+7Jmd~1x_fY)MO&u8b;L$qBUf;1jyyO^Xd4VZ|E#!3ogj} z(YyiRaAW^AYTvKKG;x4F|Gjo1+?Sq~x!QWy0Bi&mRWpOlv0uvKA`^d5mVBLF#~^7t z3WSEjva)u?%?7W4mm(9QanZiE>33yi(McB_K%RnXFLWgE73P(h3D)=TA)pjxFyESt z84)%M!4`G1Li=2xRR&*&k?eH);< zO-hi{biP&~iP3El7#*plg%E()tPV7p*!J0+rh7Wr_1Q4J4VGNzp&N}_(xeu1cv7fL zqrF)%YOPrlWZvz1pvBX6dU}l8-m|ypCIZ~Y&haM!q5YDdZ6u?nKf;|HQu>YhQ}SBM z`YE4lciRPbeljNTB7Xep(i|z%Z81lj>~ALKoZ{^fVu}!uFnN~jKf%9&6-!KGoYYPA z2ayN7VGb009Z*NGnDENXN9v2xa90L8Vxrge))pc-x~DKG3A7C;ghcbL$U7ZaFFL7k zBffH(|EaLSj$~4AY74{0skrY^T6$N_>=Cp>Vf`>a69q^QdQ<}(^HBrSOp)G;{E84T z?Nb(Rja3E0vAC@r$N@I0oQ|euNiBSFNaJfb1sMb=4 zy2}2zM=ybm*6G-K@CU|2alGA_dzbxSb>Q7>lNE!?_LxoiF}FxSY=4!#JqFQv;Sy%4 z7C$4I7{_R07+UfFYwNO32WgTKm+Fy!iCf9++If1I(mPYk;*{9mZ!>~Kro7s=a$qr8 zL|V1G@E(LRXhxQvX)ut8vCd-i{|v0o$Iic&kdO;q z1KD{Yn;H--^ml{2JiXU8cf6C~1&u8+fh5JvTk2)++hqj1(iE~n_g5D>dsC^0t9oEc zy_@c@Pr1*$Z;&=cX-oee9*%~V6lNe_fX>KBP=T!^i14zfdF2z`(rEiTE(d7a>%`BV zDf2_OSw~OW?O88zYo>=eL+6)eB-SOBid>f_qauf~zHr30BS3J(Dl9+P})0|SG0l|xu?R+bKuBT+FierkE5sck3y3C!;ppaFmW$ckJ# zet)4yaA*!#aDTe{Z}J=sW;Qn6g?38J*0<>+wAgwKf^-{sz6k)^#9NKnY|D*bA%Zkl zs2f%ymnFnWOa#c-J^}?yn^@(@o*~C?g-)^MsRBYw6a3Nf@!D^57|^5_JOQoNQWyz5 z&1DpSRxykPc_B1D{CbF?j`n(y{|2v?xsH1*29bv2yz;2BCjgBts$fphm;-TZgn#t( zP0j$6vG72=1aJ-9H5HhLW29Lh_u8||mo*!HC11OFQ<0f-@JGKc9pU~(#C1C+BRkeB(qFsjre5@AI9lc8aHDN^ApmwHu?NXlF2CTqobf+Y z(htNs?!Bg!Z&o5M?&uIQ zL((d(l`eT-G4Y-=4t2OBvoU0KXw&(e46c=CUQ+JbP(cga-Av^zGk~z9F0MU&kPfny z3)+DV?P}WRM0RcO?Jc3U+!y~OKV~@IAT&j@=zS&pCmenTI9$-~#t}4dj=;(i35c2{ z$A1OZ!{Oo<0^qgq+S&_mO>^IYOpP_XWs47KFjqV204K{8#ff-16@>ZVU-fG!mSFu! z5s{Y&rwIZXGbP2G?fsvtlP1%i<;brZ1Zn)0_P@taS24EtI1BeTWxUS@2i9s}RYpiB zj24>=X^obxiGl)-gNwV=Nh}BfnOSe!!$n({Ld$c192eSDKJ7Y1_`ML|ivAZb0KZuG zcjIn)0Rgh2BtFmrlUaW`t@PamUT}P}M!fy1=TuhoRPYvZb0PsbWq}x}LHpqCof~5< zs3;C5sr^SJ9~5Le^5Y!u0qL6c{xS}PB!3W*=Eaj(ts1z~I=2|>F!45sv`Y2oAFug& zz2=}}U%wG9kFSq?n5}e_96{O4RjIfS^bls_%Q`Zfl&;C?CM_eAP1J#)_NU_2jL7x zVRhQ^_)M*@kmN2!kCwu1pOHcy@9dQN@Qf6;1E%XQF%eUn1e`W5)BRmEf>3*cS$B}n zrN~E+Off{5B{(cTUdq^*?%ut7Juar;gL_8$p1s223R8em+$$ZON1U94nF@|bvCWAp zF>swwN{;d!_CJ?0WuR)NxR-D_=wAG5RyDN#)8J zlp+#PySd$D!}@ogksw=WJ`CurQohD+Wh4G0Z9O5y{{cbx-?9Z?v2$txZ=ujCd?BKT z;+sNO-+4T#z@_lS3Y};4#J}?D-+sVe!0UmIEq2HLVG~9DOSu4e7pVGce^N$JfAba3 zY<;LanID2w>?@t2dpNYmcNM<*gyy?LtkW_!PqpRhp zAw)z6*|xJ#%k070z?E-5Vu+zpKE<(E^pa+E!zu^K@z@q0SrwlRX$CM1dlDI^K1Tk> zXFQMHS%RBO+$OZY96NlUq`m{wVeDuH4FwvHuTfZs0g1{~O{Eib6O4mVVi&rxzglhy zaAfo1TMGDUyJ?U@mq&fX7)|69GpeuxeBxd;`H^;Q2we{_W zT6xrM1sg03eF zfJ}E8;AD>WdJgtC_b3+Dv-^sN9xCOZunkd%-!R8CfT;VqLKXSRlX3Y&YHDgW`%M{) ziVVMq@!i}*mwnmazn&)La;k~p8mXdrG=|C%pfMkxAtNeA)v5*q6Kk%>ma6fKejy5Q zfBq0-Bm$v1D=SN);y)!Q$CaW&)mK+4aDR8IPSMmXffEB}x9E1o-q5IV3hdf6!P-U& z%1pTlhwGl6p0148eA_+$b#a4WEK{q74hIKmX+PDR8zYZV01)bB29&6OPy>%imb&vw z&m=dkOCGQ(LABl4*`Z|Q1gb%eH4U}EnBqkQrUnmYYSr>{rt`zSC66d<#9Z@h9k9uO zo{OnUPWl8{L-?Pan~OPHV2A#i<%Z~-2((y;`<{MHtis}5nd`V6wcu2+yd2Hf0ca?6pV_ZFf!DIt z8LuU1({O6i4BFYj!ZQ7zSXk_(pa%qEJaCO!07nQsa;lldMQKdYx@Im@S%85-Lwp@| zXg(K$Bnx^l=>t|3%v0RnzKYc81=CDZ0Nss`MkBNEr`MI5q8?VvJU>2cy22R3-!h;TXNY7Z5sNDba!zHyQ$gO z!r)ZF%QbiYxMq7fnuy`)Q@|hCZ&$;6Xtb({5d9gib$(wPa%maJQ>S)2HKN$u9D5IX zwwBf;;`;IwwepCE2Qa5{3o#p%9lJ-LsuY@O@XFdc;yk>&t|hKc%3G2q8FkZX3}!(b4ex^4>a7!*?m7=-L;a~# zuB!wD1h6>Ccn!{F4iE1_MsETh3>$m6#E4#x`V1PBRw8cH-cLE@(_3EFu+^+_x=|FR zd<9#ZlAGIeq!=dYK#8PBFsZ&C%k@o`!cPX(dLXqC??{V${1&^;?n6}{#JgzpT*1$u zxeli5vLjrCPO>?YOKY(>JGoCP2!G5` zyYRR*C|f|+edP7!q0ilm3e*);YEIDN z;Myku=L!T({W|2YiFmy0v*3L;om%_|aY!EcUH4ci3?ib+Q{z|?nl_S7c-FYW=03N$ z*p1Qox!-fYAncwkG+|<7q%ShR7S# zjbm5~wU@pm;q3Hwq}DnBkU-J%J%WOuFBNrHx?CKVD3GNt%wvbHM0rr_#D?Fyw?xjFAeuUB5AD^Jr$WMq?qC!{mBBp}av6 zKv|dq*6#atF0rEKa6|nfP0joy6p~c)!S;q2_X}=;EDF5B(lWCF3iQy*8IYMd_C3Rp z#^xI5pwgEIES?ip#VmH9~jYofmV!26%AhcGn2YLUg0Aima*htBR(hEUfSh1HmuSp9n23_ zOqEDge`Sfvo0oHUP+KhE=vik*VoU}rZ0b=oYZGNZCd~pSS>Tqmp>8B-8ENo{ zvqL=tRHN@-@}7>BInUD}7jkJajwWDgP_4upp*Km?SGiK((C`Tqc?=^$jFuS17*;@Z z=>z))bcRcg*-gCfO`(1FZYG{*4in(KuakL*rY_FoXx}8~Y4$#dTmGBHf9oq(xWj!8 z7eZZKf_qF_7$}}tlgMs9?)z9QNceF62k=tcH9dZfGRF=(=(G@jEh*pJZbL*&ywgHQ zIBLHfl$0b0WkSTaca6yKKDiiuoGE=`y4}(Y?YTzG5fIyN88X;=#C=*lIM{7MByUV$?~lXFCbi9{IGf!D}yu zWzGb6*M;qg^5PienG7fmL|+vr`}#sBsyJtbWX#s@rfP38;(P@`ae0bBFiKU-fz!GgycN`i@X+tHq#HPFW0B#;8 z@%&Sr>{!s{b6OfDLY=d~5C8z&$(9&4Hw0$fyyISxQr(Buzhv&{Js>2pHK^(zrBAyc z?n5t##lDUcYC{eB>vb#n;<9TM$H?3S=__v^4;>^}2@c*@4W!bAUPoRNz1^4zKfa@34#o?&+%vuhu*lh~bKX=5SV}geaZg{xhqUFj4|2 z-;BRRo>HV3Je{2g%ES2K-PjMV;i9QZR6OJ@kYbAT$nUMIV*Q zb-W7~iOXo`QI?vAh}v6s&GulOA`M6Eg2&9$Dh@f4t2)b z<=A-^V(iBtOSQ)`US|b;}P2AhJHw&G&u;5?;YI*7A32IDf z3BBYp-%NS1F+O(iAiygIgVCR$QbMENJriR=1-&sC6QE|*VQ75(X~Ry?eV91B@BnO# zMH(o0j6++g)onr|TY4%#gWa2en6hOEqu#F>kNzrp`?mDB5^jAK*wP zIM?B*QGKWLuq!92V&mUcMHZbBeWWuQTU(*vD~oS+btnBRth();vp5)auuebRo0%N8 zTU1!~=xCM0p!=Lcd5edUG4X|P^DMdLM<|ehyI@sxe16KQ|>4+ z**fjhy*q1**+H$XWOB0HfC3Uw-cI@}YxI}J|S$&QxH@E3V zH`tVqm2F@KraEfQ~F4Z!F52v%>2qR32GCjrt!N10_Ti? zCW@#(i3OAOfNv9#snC`PK^G&|a=0^QDCR8oxH1M?+iJFe$qxaZ3m)js&Q(Vfx}%P5 z>N_ra=(b3+TD~3Rb_H+Uc;#KMfR1axCwfk7>1lx{*>6$dgIVHW?Hyzm2~~>gANR+# zQ&U8`REw6K{lVxfAY*ZXNy7jcbWkl{*DaY6{$ zE#z;n{wFqiJuF1h>)1n~rCpj}O=rZ>!jfa|u?-J(5IJVp5)AYE96hXpXm(}A@gT2F zYJ~CulqEue+5_7P@M+PiIe}c7E|b*x`x~2s^Ol>D)wP{x7}2u0jPX~f$kbkMOPI=5 znb+;#0W;%yjgnba|CK34OO5G7)@|tm;s=ddcz*`^O8vk3+TQ@#AT)1jZocB-g{$fC zZD4j@A*sTq4x>L`!q7Lczhj>3927yaOwG-2dbrm+IvuX3|5XO>E|l5#8?114sJ_mh zU!k#3j?{LGZ#?QTC>qwk3#p~CA!QlP%P`p7bhIP_XgF+mDZtDax5VGS`)PmCnE?9` z5MEg^U>zkaTp-9&VYpcMNHnVDS}eGL2p+gZOi$w*^qk);@+lHPyIi=4y-xui6Ix!E zrf7{8?P>?rEm!bN%kXnioSOjv_h>LMK%7=O@JK4>%9hp5sra-d1^)V5f)4@(v*{~A zrCha^_${}^DvR{qGFuxpPK!?QB(|OjyU$mo;W`W5&a~UVo5GZ>B!|w_u<~Ga>ZGWD z?++hI1chEAt)V;LG_fUuRu{K^fnM2W18kxfo`Z2V-0W`q*o>|_1=UE~=GzagY8+8N8C?kfQxLO(FP|FiZOeT2W&*h0*=PVmTL z{L}io@CVv%97&tq3Glj`2R9~jWH!_Wl5)&S)w;kcf~n=%*klMf@~#eaI7>|dSk)_F z6_@E*71^4EH@qhBnCrFNd{e$ThR~>W@dAATN`IdcG*u5bCwf2)hk1Qn#S`9vlNak7 zb@8BZMLTw}Tf6?Lb1eWpdkPd*_Wxoe{{baR|E0yJbV|7%05@C7QZR@7w^o*2Eo(YY z3QYXN2otz&v8m-7@8oW4cO8EP369xx{*r-oQsz3ndOACo#l&J5PXCeaaRIa+qP<4# z!T19;nTy}h9+SbEdmGCtQ3oWPRv=HV*7XyqkM%jx>s#MpG!!^RxXw;iR zEa{GpfQbk{biC-ni*GcMjtj3=UbP;T9!u4=a3|FV7-U7rRBx(DA;Wm zXv}!I6_OP@a(ONUigvXT)MEN!ZmeGWLMSOw9<3E{ zz{J~l*bz|G#R>NPE{4p{Ra@B|j+5w3S;W}mmevROL6wov!8Bu)S=gkiut=tz0;paA z3IG;r1}oJdpB6IXq5QTuJT zNXhZjL4dff(NG!Xlq;GZyC%5zXKz*Kv2iP;SN1@+V|EY_i%v0_$!#jV#}kl`lHz zA1)YX7M5R4O_!i|1nOsbO3)tR3EQ-__0>xJsd}Paz&er>uKC7Rm&i^6>=r#EwN!25~qjy5>` zxxOtWf)4z-Ls=+Oc49`mpBI02O4@02HUdfxe{*FIFg_V(1Zy(ge-xqrOLh8x?}OUA zyTrenr<^A!u2N`)yvFHp2|uw9OZ+GKd!<4!72QCpeRXGxn*V>spwp&&UMpI;|3U6^ zLFjFwt1Bx+%2}V<)D_=E(d<+T69}DlfUWQbtB#Iu4CiO;&A7dBi{qJx5-yxra}p8P z_&6y%-(e^rnd#Jv{b(yah2zx2CjUl;M6xR<>UL{+j=wMRR#XO#?qTNA68aW1@#OB?2;JyQ@=Idp4Aos;O>Vq zzDZnI7=CIbAmV)RWTYoJZo(-98i@^$g={CAow}9vnwoE;`(=6t(g(h*aF5q@sI!#Q z79Yy!qZ%KJ?P`mYarqO{A+r~#gSaS_D6}Y`9`Lft8L#>9P~|46(29QD68$eia;|%? zM_8Zao?g_H>$530(^I>;m#@jZHH_s^@?)DivPVkliFr5IOLhy9ANn}vA2y^Lb|Wf6 zb=rSRzI1=~@y{v;4y}4(T+PY$_V5n%;MvY~@YeHP;`n>J42jI=R7N3;nR zmtVpG8b~;(&X+Eu(U+bp3CXAw`jaG786?QJbdR68W=--;#2_!uypFZ8I7n)8;q)_! zAoWe)K&3U&ynGRcGjJh<%r@@Ju!_Ik+NZCDEQquy{Xk(s%c;&TbAFmCSKL*d-}^yq zeean~{`$fy6u(g;ogB-NDCj-b%g1MZ$D^SuR-i1NkuhC}jM|pw|d)E-Ogr8b}+F-|3OkJ>C@*%HavnNAP4P@_b4_lFu z@k)zIT^}s2>wK26Z7Fx*f^ULAm{cC;n$r_wZHX^Jci2||$b{n(BABz(6h{oc_s-{F zTb$ULAy`u^1Z}G9YvUyK;_N(mp50n~6DXB;Z;eO$-DK9BZm{EYZ46vTGAma`3VKSR z_MTMCfeDsx5;}}^=RUHl92Set;57fFf33)Z@7f)B>>gE&71VF!882DoShyohHI-$* zA7J;m2spO69O37+Fe26ux6;<{+0(`I1&N^DBzpLtvMY$M!~SYM-=|&gNfd<}blQXa zs>)5XPC75L2;OR~^x+Pt%~EYRw-?QO@b)~9@$+-qvboJCv;1qVi281zf3%=-9I=)C zzBl#eSz+v>r?K*x(g}PW)Msa0h~EPDlXHu?w#ISf#l%9-PG21&TbgE`hPp^&z2z{Y z69ssu(i8j9F{+!0cnz(ZcIlF3Y7SeB+S9aqXA-?+6G>|tyD6+3B5+#iN|K!!1V4z_ z@#Qn@X=?t8(1xuq6`aPq(M!a9>t0EqMqw{Mz4(+anaG%BGh3E2#ONaj3kLFF2rMlK@a1ztBzQ8Id+^NLTRX$^z6 zcj;_wU`cdu1d9?VZINR3#!9$_Ql~N_{cnrIm5gd@h(d!oa`LqBU8TmEcAF-(?t8rkpEM$B@Anqc>U%pWAJ=f#Qieg+E zMbo?<$t^tkD;Z_IAz9->Nq@7-y1nd?+1MMN_4^BUrj5p&4Ew&5`6A}){urW9ed{n$NAl;kzs7zE2l|uxyRWu+2%PU- z#IClyUO9hfvp~I|dT4lbQMG$NAy*4|cEeoHprBEv0{?sNCziZ8j(FX}d;$axy)tdf zB&snYhtl+CZ*rJc0z|ZMK_?Z*(Zj)M3K@;FF!{}2gJu$2BoB|rc{cOf?xVRn7{l%n zi=Q&ph5m54Y_P@E04ZYK&iVP_56Q)es_5D?P6V-)RVD)iq@=rH#){&H2MgzmUF{k8 z6xaP=WOK;uT(fo0fy`!}b=o#6kam;}`EyUOWjgHaltv-r=5Ik_l!n>lvAOwRc2uWS z%@bAAdCM>0NZX4}nMe>7o4Tb4*^B^aJWjmE0@e+2WPH#bFvZ4r>}0@ z6gAUN%WW=vz^PCm9g(I#Sdph9JU4aKrQh(4@h>LF<~Khb*{%&_2}?Xjd^H=T z-MdJOg%wgWn)WJg5RsXe>x*77WGz)I;pL|puO{}xuEAoqb;2$Uys>(l@<`j+o{U_? z6>W9O!4}t5I`47mHnXqCd%WP32aitq$R?lPU2XI~5UMXDySpJ1esVNmOkbuHG+3-A z(fdhZ=RFqItwJH5J2WbV5*M#fUa}6=D;Be}TSB%NP*=shE_&H&U?|wrqDwxKaMs;v zZ(u*QH?ien-)GXt!CrfO54&0O`C(er^jIA^a?mXECbnaLhP@H`T5p(mM@`&&N5{p? zF#oZ1t0OV{rU#^R1z93yJTGwGoHpz&Znx!IT1@I(abAqxqd7fzu(x}KWli-B3<9fp zakvmQQ;?lvw88t{%WHnp3HLWH+E)j+44YLfP_KTi;46!Z!w-FQFy5bQxGTIC=4-Zk zDJMf_X92WYcT87?m^3_X2F6&{z8$WLw5>+$$MWH*3Gv@q586?d-R-_#a(eo9B$q)# zso;_Bk>$90x$NE=^|nh1X-gB%b%(TtIRA9neV?Va1Sk3u7kVp;3DL!6+3;K~ue?m{ zm#z`I+)NK&%A^sAuAXTym9%-kdrNfX?*Q*T(U1|%?it& z*RF~*uFE^!=zXkr3QcCVe+tdw3qjSJ=eIW}9&CD^)I~9-+5OtuiHH#4i{db62+FCG z+%%Ktz0OL}v_mmtPT_YG>09}8f_-Z&I5bjpH00OqWuXsjIo?%I#6Qu-#4Iwupz!3@ zn)l4c7g7oF1eE&!_V4NE^_SN!Z4V8INcHXRy@=4w$DdvMYAJ(1)agdj?oQ`B&^!4W_KRkYaAK~32))<b7rv;Or)}_Q-QN9*$t-a=@{weJ*v8?uoo4XMH3gyc47ae$3Cvocor_Y!rl*$WR@<*VR$%=y#5VPy+)oS0c9 zWSniSfgvnK=1*f%Ma!P<^>sX`Sfl>MYEcmW`Rgljc(~uUPAlGDzWlda`#D-UGQ@3> zm^2azvqye+mG>=uM6I0F=jy7*yerY1>`uU3&gP0C&1u)%Jo*S)jBzw|eoiQiv)=Xi zg&z5A_8eVMUj4kjdg_O%YNw^eQAcR>g4ZibR)*9#8arNNH!?(7ire>y@DC2fcL^_k zTU<&VuC%+}jhfkrFEYWtI8>0@vyLrGdJ~aN7Co`}GM~2jBL$;o!j?&=cAe!d?=b?F zu5x$5&AhRoo|DRAsp_DmBF{AOpVV(AihHb%TI^$k>C?NWmjiHv$979UUFf@lN2S|T zu^)4Wpw4sXaLL~Z97GXa5!2Ra{pF`z%vH{X-6HdSvto%CFI0NbL=DNPt?8DgdOH2R z?LoCu(J}X*;a3NbgRS3osz_j8pZ|krjtMF)C4t?gCXdOusw=A}igt2)ES)pmj&8Mk z2fwv2x48L*SaM_hn5fb`vKKE>p2h46%KN%5d{A3COIN%`+f=nAfAjV6N7cpYBR{jj zyYG!tPI|TFIIY>?d#SKixRvu~>wn^QnWu%M+YiZGbtu4Ze>a_fpY48*d*R@qrKF&# zkR|l=Io$*+&fgL{()=4FG)|kui2K^=(%+IlRulJ2x1O+e^Z1!sx4qpnrm1DPM@)uy@>Qa^x-Nb2^qvkom%P=ULhK;6 z*yRzQo4+e`D7){{6K{`dx7D5&-7Npja!wbYGnqaUp}$|bxH{eR+~eeT06ez79=?mn z#is_x4nxW;u24xor9!QttYXd!Nou)!tELH@@2>*?aK(Vf|O$&6J(4!Oy0!Gvw-yC@)A; z8xL3D40$7IlU$ybT#p!ln_V?+?_jt7wtlTcmbH)9eMhAIYoxQkxFCuFQM}er?p{ay zhuXp==>@sD{+~lUIAP4LY63k9+x0L0&acfwHNdAc#})>U2)$Qo)4C|@gF7& z6Ynk%ULQTa_1pcLn+cCgi)MnkkLxgVQFCCL(ZT&a#O&lDh0`&GwR!ZX*aT8pfy9H$ z8jQu4wRXFWuT-Dkr?!}U*?mL4GqTEgj1$fquIqEzX;>(Sg?zQcEgmkZU+zcDh_0k_ zAFVLz-X+6g;vx$sVLY;W7OM-0?#RI^RO*Jo4??sNMPtxlG>S^?_x>2xa`{`b43|@; zD^YIJC#r@sqd)zcxu(AG1mpKv%dn_v{g|54R=oTXk(f}`xwJ2QciD&ToBk6#55pnz zK53q`F6+u?+P=;xJH+4k2Yl*f7ryD~$J+MO-_)gNT(wlkT#CsYRlG%w$RwUc#>{~7 z=l+7apx737oAtG|A;N)~htnVAj>BT+$3HwKe%zh5d_4G7Qtib@o}RBsoll%k74-u7 zleEI}SvvHz9WRe4YrXhZOSZK+akNJANBxsFdX*kgO^zn<0N3@ht=W$py0fvV6eN_9 z&KC;QkY>lp(CECt1>50iT3Uj4mG11Nqn(7mOm2STaTY0VGmqAV zeL}6ec{H`OcT&6*q!JA5QvckpbY1GAwArWWt2=mo!ne<6Ocp16*5S_LQ*NN`gmaeY zc1_*t)_J?c%_-Ml;-QE86HxQYi_bK;v`C| z8P!JCPOaS$!c_OXaAbL}{%rot@=87u{Sk)Y2G#xyNw?jSbDWY{LAT$|cIWU(A24Yp z5{N&o+InKPP5ZZQpY;Hjuyk;VWC~G_D$s=Jl^C#XI(19dVodyEC+f4n_-OiMvIV)& z%P;}trS{6B2H%1`*fG+r9QxNaTdJJ7e9UWZY*9OZT8m{S^Z3HnbE#fiGcY!cfKS}v z#K@t{EcM;7_5Q9fOB6BEV!QN%I6^O;#8lH6=^_De0fIqbNf`F5Mqwy)CgM2M zSedD3N~YMIf0~{%DG1b@;vQT=t>5y?r+bdrt0DC5%!p-L`yYb?$mul~C(>{|OS&3Mdhxi^K?4 zw)@UiM0fcVmD%>&Rl=uN>USpmemzUDC+kXbl^(cQY-KU-i4;tlnW*7S5it6U-ERT! zKOA=^zabgf_d}1?qoP16TjCd!oAd&@dv=;*s?qAe{`1=T*c2A`O5V`isLZ2-S3jwr z!=`qB{n|(WbYP%GUo-iwjNlprghfhJkz`xt_NT!Mv7)`?iDF$HjTP2+Z%Ar?j3jV% zbw^zS6!6PBneqFZD6#52tQy0KU@fcY#XdQfW`OY z1PRrIY8`ro=nVN^&Fyl1%CY8NcHNRX>8vF8_~e7e(;mUnAmRM;W4%Bxh)iS)Wgy3E zft8te(38L!@#X7dOEY+x=E{?`lz=}S9giDHTdQ2J0Qx&?5HZLcyRx5Q*{!Qt8SeS& zhX2>~#|Kpp{~Iv>e(9`k=e@@Hv%BTL&2h>z9ClKC^WzBGng7t=OkG?oA(n99TV(q{ zDkA%+()fMF{dDvzO0_AWJ5^_ErY~Fu0(D)z$3EAk-_Og;oY20C3MTt)>HG=1hzL8? zqrB-sIj`DXXk0vkgS;$b&Oa{)FJ5)b7nD)p73$wqOvlmDxGK>vu7AxH-(<~MkdKjQ766nz~L#< z_U09Z=Z}XtAX&p-B+bm3k$=kDe)dzhV;?*rm9Lvf-To5JBxm{%%Pr4rx@9-_lpLTH zSRW>W(=Y4(p-U!vG(#qQ6cBG9Z{-|Iphiy7W99UOeqc|r{}q{#(F$#I)jv)0a8*%4@NrkAWaxjgH}a`@ zrQF$`x@dmQc`08*1FggovCG2u zOTYiC!_JwHOoNe-Gww!Qm#CVWnoraoW!pu54GAh)2T6XXXDn(P{wd##VjU-tf1WCE z?q8AkB{_%NtLt^&Ao)4oWtZjJLF?Bs%OvlJ@l9gd9#J;(N4}YEaVk_)q{P4a%}6co zEgtDvV+Jn8ZKmpX_sEuKG+Ae!B9}ezw53@kRxI({d!JpiYh>=MOZK2<7;hAwlE6yJ zwJ@d-YN(PIc5kfG*Ku9cJ!qt!Go(ms!xaD&}OZLvGo1lM6^aO|J8LlA3 z%h{&;JNG7r$XrdBhQk!wy`6L2klP-?=7nNW0XOY(N=Ln3*(F;Bk>U4h;no&+;6G6x z&M^JyQ!-R{?|d$8S;_JLTZtqvfmh9oXjXcZyR-ZX`+S`syF1QFUpKavpKqDYeRomx zTcLMOUt~beS3Ennb(iRpk{#{khqqkU>erLc7U})6o%`Zvm-=aoneDb?dv)vyC z0S=ZkpDrHMQ=D&c@|U8Bx?eh@yshrd=+_Tb?pVK^*O@o7eR}siy_Oe|Nv+ju7UZwr zI`0f96DjR_DtEU_UF*eR=U9*h+Ph5eUF?v^+!-CRX5Gw)^V_c{O`jmX=y}F^RXP6! z>eU;VHQH2yq%R(nT|M<)T)EGl-IuxY6Kv#U3hCfBZBspCb(cNFqvx;d##T7H3{2DMj6BNIWg{Rc* zO5@Lf3~bThH4qRYrN=tV;S(R?Q{<5(@5ckX4eW2uWU9pbYtHgdee}|*^|A6}ATJ%a zkcXzS{?m7B0aVMT?`YdYseR-EM=A@gdtn(Q(2Q__e;u#J;9s?wiwML->}6Y>v)#n56vOTpWPWhkw#ETpx1#Ki`H(0#j>;@_vVIM9R@bX^L$-VP;* ztC%D6eoNe9E0fG0>0r1XB)FC(M1#j}O#Kxz2yEPxOCI^;w1Mz1Y%PG|Ize(8lHTJE z7~)?v&Klb(QjgL43^A7S;fJ5gYx)?IGV2j*L1 ze@Sw&(On%MgMh==m~?+6?LUSL-zd& zit_DZf9($?xRYSUa&ZL^4?F!Tv2|U`tF8L^-?69D@+=BPb{f{0f$ALM%vSb^Bp$AJ zTWxHK36aJmFGxUK-IhXvJ9uWs)2b7bFO+d7Gg^k+t1?>5?(%{@kMc#V8rEX-oJ2h6 z$cS*Q8;He;gpc7X`Q3nKw_77J?NqNMO?o4IoDnzQI~|*hg?AAP+`Xks?7c$>@VTE|gqztsU`_>_2{v zLps_VOxwO?6K4b)54R=G(hxc0NlUaX88H0VVM2=$A!@MO;B)}kFli_<#@VbE+<+?_ zuK^ zF!|{ks5LUsKA3IzwAcj%9SZX#$*UyU0zxRx3(LGPyw8+5`ZwJO4Rfg*Hb5L7OYG|l zh6q>|D;!F^lwZDkV4TJd>29nK!X`5$ob~Q{u!bVSvFEPS5?Qp{$&DVfgWygWgV1LW zvpr>cozQe;pw@g@+q<`!qvCG*6H_oGY+){_cHCp;_?CVAwiyYmm}Kxo0DTn0MJ@TlA4|Kxtt|>0O)MwkqT>oO&%j~I z_``=hr@Av($e629+d#=^NEJ?JJZ<`F$7dQ`S-h3de~~q0#HyzVxdLq|{~lU$SXDNQ z7iqQ&f(NN48@wR6kMD#cQW>ePW0zSOgRdbMjJ8EufU@!{8GjA6-6@CKT>DUt7F9XtC!YE$4mVH9Ub>_r+=W|AdcS1GvHwVFhpv2>y{h0!iM_1Wr$i|04B&HcD z=PjvFLR@308_)a)TxHasTn~Fln{ZBf$Vt^(oo_s~1O6H9{*mfC6LyRjM{YK{K7$7t zTG}d`+e*eO%G30olnoUjOu2ccFDPXW?ZkmyH}+aPfX~IAy13D>^217QPo#Wp)?a2ck-zIrUVD+Zl9;@@wXn(_C^*1$QUa;L_BL z7a^UL*#CJpve-1E{rfGc9R=NmFfdN#t9`^K&=MjdhuGNVP&`Km{{lk+a@z{~F#7H5d#+SD64=VIvy5>~eKj`L1hZ`;GJ z!VgH3>4ve>N*hahQ8}k{54UTfl^f>AS8xd^p5%epfj}=E3*U<@oOg1@A2?!BqWPMS#l;_4SOJ+{7xzNx-`QuOf4oW*`xb zhGOnZYQTQE4aRuCUF+UtH5eCd4~=HuKY}Cr1};@902@;c3OkByn)o<%Rh7w((<4U` z#y_^6KU~tGGeEtFifJcx&w+(aZ4=dFvNA*6Y&|ZHpE1>J-=-ChRtAVC@@XGQ_T|)2 z{DcLRd?|<}dO~f50$t(BYkJ(oF?(a?{wj;fo(p*tP`RQ8$pgH`Eg{#hFV!)bT-ep+ znBPC&vAPe#LvG2nrDz%=leb#C5%MT}5Z3#U6BL7t1Faw5v{~gA=V^~oG;3M`)ohaPm1s7+gDZlobT98w6v(Pkc~w3+{s?To!zVdH!m!lRwSm z-#zEvnk6{fzYvv7o*XheW7AaKz2*Wah|Z;3b>{Ew6hpB<^u&}Pq~_gN85q&=AT7ro z&b?NikFf07?JotsVXa*iYu?q`;CVlgAs3yPG*-wute|={1Qt*$wq|-&!3il`xeEhH`80DuT z0?~sK3`?4pn4z=CQjdmfMD%l;;YR=PwYr`xz1K5UU*xJ~Q|c4HwIvu}+H-LqhwXP4 zMcOiY*E(@vvxmo2_a567HPgu$mdF({D%6z4JAOmzpT?hMFy`m*zm-6%6viEEFrOn= z;xPSpKl$Ul33A>y?N-$e{Ul{zsVv2+q}`|v(3QxOuIpaOQe3n<%wr@tEx~@M%1mVb zC)CkG>@vKI+5Wb&hs&tmGRh zCY)xbuo7~TEjUT;XL3V}ctEgYkBbu_AJE+RJ4qfgzA2+|*Aw0mV2XNPpwrfzNRpa| zB2iJC9WOfAJmlo@eaCfAviz{@xK2(TH+J}uSYhb?)2iDVvF@;JA;Bx(xG%T&5lQJC zWudn(Uc*QEickz}*I26(83BR;v@pMTR0N*L2G#;Q^pEArJm~bcjvP1{&+yQtV-!rzMBd5)M;nDb!ZQmx6|YWiEU_#vmDHPbOt;hQX!WJy4?= zF};kZT)tZ&3oaol4o2>n+$PRAeG7vfUWunHN&`RdFUgE@OK%AZB)u4Ak`%60o|6t( zpS6Ws@zlS?Lgo->r3QA49@Zu<=_S4WTZM@eDH`SHd@qUn;>W>U*oN+Kkv(T;A!Wz2 zmzR3-RiaEP5v~yd_H+_y-D^`D`=3q5?A<=xWQw4FL+U{sPVKlkV0b=Y|0HlosJVRp zmj!J&h$s*Fo=%2o*ayIJBJZ@LMV# zUvJ}!O(Veyv9QeG=}GgCRS^(fnmnh|>TPmyo;5)<(`BLbCnuHA5cux4LowHVdD%!G z{F9vL6hw^_R!J04-{Yoz!#(7XH<{ZnM0i?~WQ+Rfn;6aVE3NEY{$Zj;VX`;|g(M-z z@hyuQDVIEmp=-Z8CRV2f*dOrrO!@ZeArz6r7g^eF^UH{8iJ&6J2m>C7q#2k$!!u~S zA!TzvZu;raU~Y6cK1aTWOWb=;X~b_z!fLOSyVM%VV?A|1uUeBuyijvfuuCSFLPFCU zEntGT`){VUIOtcg;R5S`wex2tbE-E5UV-06T`# z1{LO(7rBp^$cTlr@89JcT|VTCTZHmvl5aZ~@|QuUw+(DR5Buh}rw~9c|Hjx#45Rah z5gSHLhZxOMhlSVwlaRhPB;h6W^*{wu$(?#Fjgn+Ruon>;x^Y3s}`G|zqk_+pchHHn9*j#z&lB^5lhW*h)_Om@lZhG zNAW&ZMMCw9jW9^86Np@0ZJe9QcFj6apJXNLH6Txd@-2c=;;vrpt2vo;9p}(PjzS&( zxAjSeLL|_oPpM+$wQvtDY5WXiMRqM;&TK_^EaX~$b@4%7g?83y+AcsC5~!eAuW+W5 zTs!v>9d$0ju{W87Agd(e(4d^#UE@~*Ff=r@0h0w+7`M^`z0ZZesn?!2*!5!|sf|nl z9S?<24iav#8v%YoS2hZwE8!%XDGGKUJ|^ha5r9ai6Zsy?I1A^yu!1mYo>x35?`Sa0 z$wj`X(r)F~kEq@Okq=n25Mn8AzMs;!kK@F*iS+-38d``oCJvyA94Nm+SfkSW052}r z*xmg}#tq`{fMxs&OqLy?77VMuUrgt|Ce+uMg` z)e<*cvAl5F*dD1|RGY-dwGftk+|R6;5>b3IPvMPZGt4`;wix&In?etVFO|1DL7&Iq zZhL%3O#6^quiHzw?r^>k8J%cV=1~M?(Dx~M(xa_Gy&B&5z#J5%`}dB0=Ah2NzsDFEBTHF%)u^*|73@t7bN7j6w%>D0)uA1+e>B#oOcd@QuXw}!LDl)?zrjl9+`Or~C|#cAKB3mrd= z_Mjl%DkKr}!vO5N3Z}7dPk>a5nm9wx+A4n7;g`+RU>_1-hlt=%4i}hmU-+X9xK+N_{b%F*6Hd7iGDt;-)dRgBZ&Q?RUVfs*H28iD!Re$ zcY}%pmx7$I>KwRB!OHhRXvniF;-po6Sjsc&Z77)tNh;X7J=Bj?+2=I`t>^YHmW>wl z$5ynrMsRu1r=G^k55$m94gidNLZB~#je%DZHs(5w79i|>1&aO$2MR)qA4os5rYA7E zU-ai@V(^ZLGK`5qiqxi+RzY3LshC)kjGfTRjuY>37~F7d(-c5<1_K5*qWgQw_2Jj%_V#&8TVKG7x$haD&!IR!WH=&Y26nIszwz* zt;HjfAU+m76HH!tyIM*>TD~T=^4@)nHcNSiOlgzC3v1F682z64s=RyJ zQEF14lW(>O52wqg|ALzJ`4&?%Bv4`HMqE;z+po4+M0;S)*!j8XopOA_3e zCx_Q0<(8u=CB9wZh4lpr7us~=uL6e~`}Um*l)YeiIkOhOn^WRH{15lmJV%5w(Lf?> zU4}kI9857SjI`-R7WXGv5&2i~@Fr48?a#d*+xbXTBKI7xK+nL^af4k1RxMFtr*L&s zLhkDJ#D{2Nwy?stQT%NH)X-g@84fWi{17#WB6Sq7h{!x;Va`e(3d-JSm3e%j{2D%yP<4QP=&|9Ip83IjsaNKp^WRuEoZ?ijXs4@lz1B+K zjbgXgi1LVYt$b!8?S9xPEEV4W_;y-qdn_1tVuH0G_DRIi+c)<-a9nqjJ^>hQe;kwj zKHuIK`%t2xj1i6hK9eL2+nuVD>Y0LO9DKBYw#d^B#@+7wll3>U=&-hZ$?_*Qcl_H& zN$!NwjJJzg6M3ec=y^n|AT(ZYWK)E9Emiav+m+99(Oo~<%twkuu2bU*0Zv0vHviq)*ihOkk(M`8&zByEM7RKmTenn{ErD*XkR_oWKAk5 z!A;1islN&mcp;h+cu6U^!Vx@Vc9lA~+z@V9exj(L0lWF#RivnX9q+t&lQ)WZ?-8YQ z8pb4#7PMm<-h$ZDnc1YoiD>|Q4p2bN& z+l*GhVOVe_owZ;uq&dUnB~tz@_1Pt4h>~#$`$o7yu-A!uTx(jG2VmL9q(dBj$TXY2-CO8J&C2lGq_7SYF#Uf4|mAG37b&VOT&x&E9PHdjnD~s9R1oAe@u;x z{D~AAp&4>0RvPQCq$q}EYi~H3JafM!hPP-gFU64atXd8Ea$=@b#$}tCMBqpYApWBs z2$YZa_oqo8XLHTt9j3x{RS%Z-{Q}tssfCF@O%Jb9L2xYN2u-4uxBDDAXvJqO{#YHK zDINuqLUSm-ttvf0MujAnOX`f#l1eAhL1Xj?$t>O8=0k6ve#c}peuAc*D@>~(Y~jUW zM?!h>6$vZZqwwYPPx?Sec=i^jLD~*(^pc{z^1sZ_96`R0?X&u;{JDrsGG5-$bDf$^ zblL^%j70BiqjznT;)=vfIeiLpwS;T*1tMCqMjMF8HrsDIz_72tvr_uhzW%uba%3JjFPXr`tk(Wpd4JuU5T zDZg`-OJ-YXKrF3$#gjjSc>N5k%cCfx6cr!!8f9QB=L4v2E2 zE7i!)i!ke*)STgBo4+<&8QTsXCiFMLL zK)kZvQj?rvwMs$1r1Cvda$F12`nv`f#z zhdV=$F|&@NF}7+kDAcboZ=h4!viNh=LDJZ-p#(fZ>DxeH$pr!5tjf+8g@Q_eK$_~! z64^u)20dezeZi4`T(Z{uP*XwZC&W+{Lhj9j2RxukLwoAx?!T@qHS~V42WQP0M{Fdb zl?TY^Y}Ig1m~|4~M{8P#%FM1@M%aF*)j#L3E$^c(hp~Pj=_Cq`^-SwjKS{S3F{M9q zX|eZepu(j41#Re1jCr%e`ovp7XT{sqTnnX6rVDXBU`0P-nn}$9)}m~s?1iF3N+64j zjcOQ9VN+zY{vi}kOUoOHHNt4x;tk+h99u1+__vxMv;)a~e$bsd(NUqHDF5hBj1csF zLm4`xdKDQ>BQV6Ks0S+#mW>wMChy9v$x`3Ze1sKc&fPp*!Q5ek4B5 z4mx5!YJx$QXReHPxs-XOD#r&oZnsdRKi*`GK7fcBR~=m!bHpyR6JxY9#TU*E$?y{^ zuNP6WtPd7^$Qkn_v{^WbicdHA7-(gEc#D8jtKdcVhzr%+yvq>>qZmlU*qN`BHjkPS z2y$-XFF5(w{TAD%{M)U02VWHGdE4>A_A#ruTl{C@WsJ~mHp$kH6~i~q#v8$W#gIS- zbR_*ONq>;+@V{Sr1mm~z!ygJd`4Tg83ol1bIESa$e9(6HjoetJ(1<~`_>$YL2y08C z{ui7#ycGr;y%UoY7_CJW*4CEJE=LvBaVG^fWNE3i3F!IXTFphxb`PRPUWp~~aL8Qm z22my+R^xj}8b}RGN>rxt9VE)vQaS7IOci%rLl;KFJ^bS`eY4BF5hv#ps+43>=`{#x zw7wx*AQJ>Ze-z%3^=XO)y^Rkbp=pKMrRleEhaFRaM+1H4*RGu+CU`rF$9!% zOJ{f;VerAiqSUIY?L%~g}z!{otzN=?ZIR}S0duQEUTX)FG2;7a0x@qT70n2pJy zsfd#NvB2mZHjqYp4KJ zc(>Fu>nOb0fH|~n{+y*yw*;t6GkEj%m6*Q`BjJ#j`|rvPx0dpkwu(;82p#bRwpo`1 zcdSBb68|au{38hKt+&fz7C&*|W9Jda=DG@Wkv_p9i@tKz4%sjucs9?jG^*j7CK zJ@Dj~&;tN=(fmFym(`Nq9A=-A5D;8Pms66Y5DcFK?3x`7Bf^mJf|0s0)_;vJ#~QC? zoBiP)fL#L7+D#ly`S$9@=g??;F1HF-!4gEG!~Xx^vtB~JH<#N`ixN*m+)4_Oel5i? zlVN%SoZ6FXg@OMe1vd)p(av}oCqFOKrT)Gff}|V*Td4{VEmeHDw964$ zIwBHT%lRVKZk?4;ybJ;ZZ<-IDRxk$x1eUj0@A|$@uy_huk}K9R&2eA}LLgQo+`u?! zSN?FU!T4{v+Ibnmlf{1aNXF?2tlwRmfCO3CzrCY_7JecUqSa9J{)6sq>eP+Tw<6`>pvIOXZp@?X zPp-!j?MJ@>f#G2Xa$~<}`%lb~MGTicBGTxX|fKR_T-O=f5aZ-oV^ z!rcuHD^wRFOk~5GN`A9`m;GD-C#!e{3|8{+APzUio6q7vq|so(24CH!%NNb?Vyh4y zW~Gh4%MIAd2#-7Q&Wu*t^rl1zf(c=O5&0)Anfh~svi9Im>ZkG%EV!kBF0nR^89|o1 zXLVTpl!fPs=-SEbR`Jpl)I%~UN8S<4Gl22UWfFaXHpBogn)e8{vx z=$|ACQN%wx&4V~iyNOs9St56)RClJ-Xwv?$pwA!ZLjdMIl!jHV)Iwy1KlG7TNFD{X?1eyNbsx|U00jwE4C6-gpF=Kb8` z@)8$5d*#^KffErIee$@h4*;{0jeS5vdPJ7pMU_j*iqbu$`#OR7AC)ovll^ykK#)4^ zmu@@+up15gH;L~{_1HBJee2zF6=6A~Ty>5RA*%!9FcWc%^M$_i2WwBvKWlYY7;nV> zlu;nR!lw-nPp6V!=OS-+&(abpB+^1}yB4hBwyr8&HI|i6eRe4XP7F|t!QvEx84Zf4 zC^yLu@`tGP+hU(v^O}<0=YSmguS%cTnT4hPSw2pi}H& z(I@|1ZsWQVZ^ip4@2&+PmAPh16@h1%$vDa1muMQDb1D3jXXaK72xTd=6MLTe=BQB6 z146Et8qu}wqyb)7pTU@kHbS*Jmg=p+#XplbrB)5p71`HLfT!Vp1dpXu8UQ@GHoVOI zLAIr)o(^}BlcY%1-=D;^_PmHpEw$wnk=~H_U9x=!nhGk7vj}~ZOqV&!0`(7`-x`D@ zZK^%jLZ8#dc)2ycZi~VV`tBmCWPdtUoPQS;G}>MAPt3+5q2Vk57eyIMYIx*?x2q8* zCOZ?*X$oOtfQfuQ_Dk@n>i)tCOf8W#dCYwLfjA8u8y5-C1 zv8Fr*KS`ZfS6W^LW1tE8nRD~f``kpl3pm@n3$(Pqt7{&`_y70|ev+4re8OX9m?}c* zn0E++kXspI48|$G`t4Xa55bgA?VDa4*1no-#B;X;SHiI1xb2k`zJewj*()rIp|~0W zBaGM>HuY#vk9nAh2E*3e00d0~({3=i&j%=NT~}qanc!qbTbX`cZ3Qk6e}1}bz>k}m z;UfF0`{HW2EYm2nSMC=qT|pNtg9Jmfr1WgV|rJT6GjU`)+X;L&*q}p^6>r^8wu3)YXIawuyi#Qwsse4 zM6&&LF|e~j(IF9!t>JBM?lqgnG9DzUgy%@Qn~*P?Q;rr|dU4x{U_ zqT5IEzF%pLEhmeT#g?V|9eBsbe?7Kknja4TbnmsDbPooh`lr0ToQsHFM(*7Ac5Zyq z4KI_=XgI^;xbYX6mFt5v5ZrNTe9oLvSF3ZbXLjeoSmS)?RyyJ@z@dl!Fh|NinQnt$ zp2M~tzbhBQnRR>4e_U*)4wl$3-|;dc`{hYAv7Mh{XLlxiy$KSyUHWss41ylCH?NIS zG;c^UYju3sG75yQGgsMMk5guw3#@U&JO_SIb=PRIg}`+%PCozQ1~=@QBX*YH-ETp} zFHRrV+uTTI2OB9u_v0@=>JvN8@K9)2_sIOf>ElW06lsZ|(sa!>d>Bi=H@YOSf4HR( ze!VBu_q*9+2&3rX!CIvinZtxEg-dP1^sG!No}Y-wS%`W}JT-7KGTT|!*-0Z|CwW;12*JNS=swE$P?uTJ)_ zCe}f^JdSR1@McX*7vm4?wyuHnE{cvdyIG=pZoy;Oyemu z_}M^;kX~~!?*!^P_-!Pb-TbRZO4qum*=P8wYx1wWlg&$i(7zYz|2}aj_6Dg`y2JHP zKOCv~_XHzjemT3oy5G?RT`bsCo0Q~Oq^x>yr4I2twwqwYo3^MsK_Pgc!o)!Ly#NblV=@|V7{5Zyfo*$adEYwg6++syGx%Zj^-qgIY*s}et#R;lg- z{&^i6`o`_6|B8}F)=*5lyEX}cYPh$*Ow~o%d`>KA{F9vkHO{h7UA-cVOeNFu@qr;j zSW9tIJZS@(-%8c76sPQH7!104d?@XQyqoRFAx(byJi!wg!v6BDkR2N^*Ik9C)w2-| zZ<}iTNs{1!uf@g-U5sOk?eL}-<@k38YO~;QdOS3vto^+0DfgJXucCLAJvYLSCg`vd zlHn9%Fd;!k^yUqSNryCBN*~UqQ=e}t?|(Wn5P5pl&Sah4$d8%C@~A49x_q~){oNQ< zr@kvBuyku>k@3A(?QO`Gc;wT269~>)C+BAfDYSr@h>8qlp}iw$zrOYuKyfAyGCvES zqB;BHJZpCQxLp0Wbr<9IWlN0%)@+ZMyM_HJH^crbW+Lb6;86D|np|5r^e7W= zEEe)>sNnkcX4{K0(>{p&uGWnKwj3qEiQu~|J%|v(AsRMN-fS*8g!P4>p-51A{Xb(% zy>8yT3e_M)W0@`#_aqJG0+wA!R)y->z1AELz#Ov_#1{@9G3e{ z^zJ`HN3^5CUa%<6K>i#Wjz^#(U$`%~aHtVnwPUfOFQPU+nQWeNcd8dcM0;RL^_jqe zIqm6IT4o$8NZQ%6@|NEqAX(eci}mj1s3g~Tt52q1Bdzz=4#F2N@+c7f-oFMg&YQ&| z{6^*edKpiAe}Ks@x!4VS=**S>nYEEn_8{t^GB@pt&B|0KJM_)OBm*Vf+9->$IFUb7 zPt?5GTF5{wx>J1>mEdtpHu8yjM49+#a%<_2STFX*{r4F!hmKc9pUIWn<_)0G?%8DK z70KPkn>zHV+5VA_{q<^(rg~##WyqlZ_5j)rWfgqOW$ze)ZBaRT?`|35T5l+&ecqco z-{?og6&Bw8$AVIP@f?uxU$pJ^Jp>+HHriAlWb>~j;fr_gHDr*}8E*0dU_i6$ng@ua zEiRwjKSg8#!R-34IuV{gC`@(R6#n!?Xo9rA>W)`1`WK_@!i*M@1{geX!s2N-}#Ij{_39Y$Q zkxHwMMC(Ea0gC#H-(jh?O;EBD28-_>Gbl^J^8-Tf=|(|6}1bHmSV z&*D~DLzhe+^jr;EhZ0v;&aj$Z7pJJ~#{jSSpS!<6R^I|&Q1qD~>PPRKSkaK=pNAHO z1BAS`4Vtc!BiEhuiTgPe+e|USfVAJA9P?JWLK0LRo`&Y%!TKMcY<8X=#@Q)U0}kN^ zI8sFpOq(93vm5rZ@^6eKMo^8{$^``Lb^8gGYgdm>g1Ild!*zj{ttV~Ihp2Dybhu(^ z|9!K&Zzwcay218>c237EmY4HXZz+1OcWTc;+-hR!ot9KRnI*a7Z!a6k49y}3iE>{K zDlk2-Cf^*Vbct)g6)z7fjo>*jcvw^IC6Z80AoIU7%+viN7Z;oSPhM{?neMM;a!sps zpRV@GtM&}5JJ%gvAr7ED7h=P{^?KaR0YcS%V0%Eur#tnrhtu4*zAu)5p8p#DyRCTt z`yGEe)zZ?hW3zPx~AHf=-ijHBYc08S9M3ez1~&+R@z_lIB=OkCg#4YY+d#2pZsuhx%1`j6!W2b zLoCy2{GKff`J8?C#IQSPC;o{D)-7?%+iQfQKI$4rlhv5Lwv`^#VPl;I_1}NKwr3l} zE~~9gobJ2jY~X+NZA$b>8Are|E){!?gmgdo!4+QXblHAc zTN8m3zM0RxhgN#jT(JpVPG&n8^@Up4zucY)Kb;yXEe`jEMc*%~G&@fBqB@V2GF`5^ zfUb_BML{_m?b$nO0&y-Nk!bMS4GO>SPIl`JrPn=p)#)i`etzC@dC$;nt?fNq_vy!d z&>NK*Wgc5UAM?EEF`^897EIgKXNWs%^nNE z6`qJcf3mK^+IU-Ed)_|iHi3=1xV)?HpZU0g1?A#@vo+4%9@S{G=nT7N zVOd$!`I;^ITnpi;|I6_xX$afIWk)9}TLP4(3S2#`exX+LjEH+Yam8~??HRJR34#N(jj3S@q{U8L`{ywK6S z>BmDOU!qZ=-@J@UZ0L;SSuU#89`{GM`FKiRTSdTtuGwYA-^5ysb^*tf$u zu3irRJHlk&2fO(9f73QhBy>~x6yKu#{rkTjscw3L(#_fNu95#9+AC0Zk4Kl!72|l< z^KAJ%80-Q$+koGk*Hqi*kAF#vii(22yR6i+-%gOiL7>K0yZFy(t_+_u5OE(#;7VPy z#6%x>n(PQ;Wn1rnR8Gg%OfKWie!bnBeOCfc6ITztB6pY5bluyN{BMXomCb8?^QD?_ zOE%}r4b6?XrN6_CFH&u)_hE+Ze^STFz5{55W9@>aQ9Zha|@o9G6j%0#!y!~S7be}JGSsl>Xn;$_@w|rn+vvYQM zxZ6eb5*knCMt}F(Lxr>xo$d=%>heUsdpnj8eaXaZz28>5`lnP4=Xg1o%hr~gZs?r~ zHt-1{cW480PYGkQFdXCr@Rj6x<9KY`AW~*t!AV*L!a}CnW|mENObR7at}}rp(4XzU zjLmI1v6<6)j^Nm@0_OJmM-OK)36!z)k%LaUDUCe7VhXXQJ_-}8a_Yj~aia_FRi7c} zB|*D>dL0|@GEY@3@_c&6Y{Xf{vJPm(B<{zvu3gK_J75IheKzGn*Xu%?UE@!TgTzE z#NFjgJY~X@uuBQ4@ZIXwY5PVOEGbW1$gpK9*LhyfPB_fDxS^>V8iH0^&tVlhA-9`1 z#G`vyhByZ#`UIV<9i|l7b*6N?9)>@JLW^Nb63@?5k7?L2=obE>w_p(NxBp{ltD7s} zV4_|X6zM~oX!KCG_rZIKrb4Hdt6BehOd6}+6tw;PdxI#W>hA4ke>1~Bmb+X?Su!JA+)cLK*XnO!1X2=Pjb5`j&H%3)k6eO{hJJBhT zJ7tS=-snHv!!^8SgFnDG{@WrtQpb*ZXE5^SH>=8*i^O;+=fhi!WyxW&y3m!#%v<)uJ;Y9uC6ZKst(=&w4|K;uJcljdouX%D~uKRQ!iV6z7 z@Yl6Hw)uDS^YQJ9RvYX!hjhTAjm*xXcJ&WUjaBk&vj2uud35(ONNl(-)V!KTxba!H zd2$~Z(HqC_V`qV#$ZEa}17|3N!o`OUNBP24GkD->@`}v;RH;vX+Hh|}z=RT#4{P@Lri8a>}TRRHSW z|0}Sno8k0#u8l=h57_4>c7A<0xadWV_PWevzdEcA=!a!=&;4aFd#8ucyj&#|J{1-Y zf-s7OJi%w!(<=H|uL%q6_P4aOxVV_CpTqNVF#S=cBgG^hmdbKG_G@X>Hnpn_`a3+< z&A%H|wC)JVRBqW#!m0oetkx^4t4j*O{qn0F+7)udU3~Q#944jXV|7|LJ_XkybiSrf zu{f_)Z@bK8;5~ZocykMJ@p`3tlLouN%JxBC!`3KFHsSciMLrk|f%4f>(5}|=k|0j> zky%yv5-BS?8$C?wS!ib%b;%r@6Dj?%WvA)9-h1I^Val_^tIzXc#2b6FG5|nfIvi)b z-r-SN5wknC41tVCvv)}w8oEZu1vm}KI1lNTIE~Pt&hQjRLZOt4Re*Fo3zw;W^*E0M z<(#)Y_JEUSD6ASaWoy4ydI^Sy0aS`q*w34vQu$oWsc_r6=!LCJ5%%PzH(xeW49kJAK$S& zo_ork=<{!%_2uWw(UR6(Ol?P(gX-!3)dH+P9N?A4TrB;G=RVKzcz`8n?6z00gWP_q zT;D8qz@_<=$hocX5$C~{9Lx*?I}>y8|5K=s60=#pA>Qr;p$61a#QmfT@ZV?ec%J>_ zdt+m0ym%^-`3Ch(W7nG+%zaW`zMWYW1}3Y%(u=)}vdc3j|L1x|O6ix$RByjc<3+NP z)n4b{EYG`k%05yF#05aRdd$%T)SaSz^1uH~vrcOnO)S2ho7YdxO0}+Xx6blOFSbtm zktIaLM5~H-Ck~%VlRe&c^85;CuP-r(K6~Zg>5FbD0{U|IuM^}ofisPSsT)9-kjTgbC>uhoZD@T*p~;YDd=hyZ;(6m?6v$dPa#O;8_@Hzi*h#h zu6*V%FG;NId-fkc>nB1waKe^1ocG3N%RO-Y4Vx)j^;PRFRK7TE)7yY6b}Ie-Dk_gG zCbQ~%mqDL@w4|g%8m7Q!*Us7Lf4_H<5sKyv)NDyPv7b%dRtllDlMP){bxqb6P89C} z$?p?H&bGspCZ3J?M0_6pt=ODJMn_v7;D{YBl!w$uV@BYha6E=MVSNw?ypxOf&f6*f z&T>w=J-8KK6#UCEKytS(SS`wVY%Ek@863`HwZ|Hbcb}Kq#%NHr*t>cNQlziAtEK+ap~?(>8?w6zs>hI zZ|1%C$8X-7c{9#%!E??%ckR8``o!Aj_7~9f$Fmx?`gM8=?=wOg8dHus)gHr9dW9BT z9UK^p77p(b8c)%!ykcT5Kc(M4uJ__b_8W0W2yfp`rJ#|$tz5Pyup_Yk=ELTiiX)%S zjvXf1zWGT@YW&-#rvDS&iu^!8LB=hL;{afa**{ec-wtKExVC1M5_m*dD}S;}-X zRpSv$QxEBlgH4Tuw=2!lvGq>-;w_o&OuCh^b2YYFP+x~j>D=)$vtz-g>l+V~!GVM1 zLNHcJxu42)kD5Hb_C+$j>@j9{Ki_@t=_%M;s6E()KD)qHq*-NjGV!y=h#f{s64~3* z+j;%%nL10t7WDeihWTvuCG5xH=f>vRj@VpPRd;uO$uBOK1MNLMJrD7Dek_sM+1X!g zZPkoy-^KKbD=M-V7iT}*`+7X1lBN(Ah9e-@dS+{1qs~M{MYRfpDeLo0-tQ)*rur%7 zR}wOd50&XPS};!*s8xk?l5bHQ?y6YM)O+Ekr&kkPL`O%DT5!3dpCiUW59Q>-r%+=t z>wR47=N=X{Y(zyVvSsS#ol`V4G^VyH=EHi8D1c9$gY94{xFA$aMTJZi5*W}ym6}GX z!0_nhW$Zg^C~4ok(!xHc=XBv>##Mb7v-*0sPP}%hdmPPmWtRJHLE!}g!f25e0fSn8 zM=x$JuhTZ$u+Y-ow;&{x>H|C_dG@w>ZIZ*AZz|eiLJ3Hhw^%DIFP{R^AAcvVuaAX| zjXiPpxA$~dq&qUx|KMLV{%gOpKZnG_o7?(9l3s_Z9`-bC_rl_}sBJzG+AQspvt@Y& z#~(UAe|V1-AQ0;?hUbj;Itj@Y{3fVPLYkg`kO0MAVL|eXY#(hM+8o6nE;qGJh4PVx z(BoyjeI7$j8kwm)m#H{QskZyh{xBCE#qZ6_j?UhdG9jOQ-mf%mLX4Eb{aZd;yXVeS zMiLa6UD8|MB~?&jBtA*T2aFoF8p%;_<}jk~HpryJ+pcaa7v~kQAc?*;%~kkfn@f+a z^VSV-Z$w`^2WJu-ip`=cVax0;ezGBkmr;W0z6J8>KGi|O$C?GmVP%SJ7K{?JC; z7_Qpk2LzFYJA+QR#ZC9i|a#4H0@yg^>UDY zqIl17n)OE$0rr94XNNo$22P9YbGNtD8f9pgJx3XJMAfnE#Ta>bJL*P6FY7nxjAe`% zm}TmS(y}NW>d**jjOgBKGJS}Lr4VCesJF5_!hc9Y7z;z73Q3(+y0>Rgx&F2nogQ^A zexZOBKudM0&@#n36`2{yNVlSqE?&mIz5&WuCxT_{>Ne zNss4C5+>uHmd1U1J&cdq&JTR8Lczjl*r1A13_iT1P;Eka#d>%FM6#FBXwv2Df>NU2d#xy#DPzvg(9k3~ zB(spEqYbKkwTn9Cl<>1OzRBjiV<$wes|-w>nG4VnR-J&-Bl#V5;HB8f%b_gaZPY2B zjjZHxXDW0XC?wMD-3TLO5fb9yOZQ>8hQK2uR~Si_C%#Py`+$#})r;giKsQ>pFF?1B z0&^x}9}k^LW(Q6d0ka5$LH%M(?RG7Nzq?WX$6&%{-=D}wohGLS15=ar4xGhdXYO}h%sxbiti|t7KWk~#OtA7(+CC_M2OqtTh4l30) zeI^N+@5F~;3sUC{=@TU?DgV+p@)h#YUbC<$)9rnaX(S5d^JmI}3z_{PW8P|58JTYU zE?SmuLvtO4n!U>JW!BF<`<1&gS7NanAHNha`o9xqV0`}T?cDkoBHco2*Hd`=&Um+N zB4ZT^5k2snWq8_NYQ>HBYxzzQN(KHr`nbhc<@i?0=KG3G?z19XU>@##o2wonN@<9TnblQHo4Bs&FNwf!GiOQD%+x>s=Ps>5o1+~mPQ%~XeOv&>J!jP zsiucevkM3QwUKL3m=2Rv;cB=SB^?TqvY#7D8@!dzvWfRI9b|}gIDf}n(_Q7n9+$0b zQSFt~QEMh>(I8i$DaGf0@k%o(c}_aY45o+_A*?h3W&O_-a;yEwZmP+X^4+u({f*K8zc+%|5`rUI zZC3FHx%nR!WDXT74AmH>-J`^lNNcEQi<4n*%}GiV%M(X3k<;|@{9|G`xGW}eb@r-j zp>drHlVKKdyKCxw&jZtN@ss?BN9_|iS+s>c!cA74Z#qp$Nt!v=gB6RJ z^B|Tr-RNi~)J7{A3nG(>>=rltkigi9%5HUz_jG*0wHqvAnVbfkIa#Xtk)MC7@cYSz z!=pxPf6I!Y9NblT7e}4>?t6|Cb7)dh&wxf^1-qS#lW2}IWj-u2X^@JF7Ms*Hr2?5? zhr{!QMtE`JI?>BSv&n&k@mvh03dv92b6f}nQVF-h^kYWs$C}+d`Kndy{rJO`!qPSy zf1L|&oP`S1Sbm9KgT2{z1G6D?hYImq$sS9tvum`XiJa$JXy0K1_WQ6(PriMnh7``P zzu4vBGYqy^eE40=fQISo2_%U!9p^;4J9>Kmgd;iq+BMjZ>H2hwj34dNk=aoRY2$1Ssi^eDSc&-c{7#8pKFq1j+jME;vb9`nz zwt1K2NyB2Y_IvUteXQ1wMzQek*^a+Oe0%eB^&2#wqWQN`@tySd%TEYuX92hn@?DmY#|lD>?T>ZsJS?Fb(36lTxscE#`v{vgRL z$v!@gzCQNSvxKlp&tmM;=Ev9zYhC^8zs)1L+0U^YE^s(D?+vZ9OVpttZ3>w9++%-{ zI{VUn0AbNVM|>m59^72LZ@ezhn0P}-j)WeywD ziR@gp{B zIrxnCnUSY$r=OCJdJ)YyiF%Mbq?MW}x3{JHVqMneN@x{&EBpp8N^~Mxthi4t=3Jb< zHZnu-`VyWFQB*po%KgKB(=Oex*o1zIiGV|v@d0y5PWi8LRNZL;9NSsi!`as?4guHe zim$T_CZ8Ttm0B|kZ@=I|C`De$Ki0UP$fT%3Iu^sfEH7i$x-ljrobMeCCg1{eekmwP z6Z5k#{q@=2WR&?5bF~~MYO{2F>j$-r1O>YFhQ(@X=mmnq%87F_S8>R!5e;R(Z$Ln= z)n?nqUg)F#8=|meA{8O?&ov0|X@{0o8ri%3hZVB^(w=qjg}BjgmF`1O7Qa zJ!;I`TfzYpzBj$nT#3>$a+}JY2psYYMyp-8+P#%_XY(2u>CGLo&1^DgsXr)E|qrJ*|pW zQaTTPu)|SnFFuvL4DZ=+@J@xs`nF5KCDt6#hG!sU2HDakb6F33^OsU@wl8qLFST6x zle23+Rx7M9T6V`~Y0z{c#2AmL-(T&M_|_2)E+|4V*X7f9bT4880@F}QN_zVI!ll~4 ztrU;7W&L3?;s=w_=r||X5d#DjzvK-E2dbAQ5jf^mC_+QSUoEKazlMou{;J1)n%Om< zNX;sXzE60AXh#1K%6aA&)yx8jf?|F&5^V(@eOA7P}{d8(wVTm(eAq~Wn z27OXu3Jfpj!~2`kKQCrgI^PFx^yA=p&D9ry7n)mf_O8!MZT950Sac|v z?FH8x2JhUf3cj$tDl$cYJRnAlp47E9i@eCJr9ZKo{#R$nNfbpz82zGafGJcX|AQoD zH2rhwOv)=4eqz?{ENOYu5Ae)hGW3M%`4l=9Y7$wcb^`QKj-$QHpM!sFtH+}7nZ&?l zX+Nt&Nm2~|X~dM1!lW(~&>v;`A)I6E?`(_cbcN`(gD0B>+f!644T}aFB1asJjEvYV zuHHzZ5%)CkEj?SVF%^M)S@cP0o+?@1hifE!%LE)b@5y1nL{`)6I5gw(3} zU_@c-^K$3p`b(j8PfNK)W5-91s#I&9c5@Fe` z#}0bNDN6Tm5v};J-P(_gmL1QB24_#6hLO4Zz-Q*+xz&U!?tj5*v5M{)80d&h z7gu%1$M=T|3=znrmtm4bj!TP186?7u`9;e~9=M$-&XB;|9+&unWIt`hRJ*+*nifU+ zlVlD&n#QX^!dGFvc`r2;%g1P>jFot?ec$G%@)j zdyBn;lSxXstTVD7<)pLSFVHnV6rL zjgby3tN-YSqofcJTAUcgAK^bK-}1#Ndl7}#`>_71HlK_gHf5p~qKvtRqcu}6G5Mw= z=yy~|cM>n)n-UV9m#DQpjt*duKDTtDEW+5BqMgG?q*c$Vg{+`imF&D*v>V*kj2BCE zo)ap9^mplUv0}#+cPCC$R=u_Y6ZCZ%_`Y>cro`T!zIWwjAEbYF>!Py+HlnD0mNch?Nfw!m-tAbaUn(_O_ibE0K9rym zuyVMzIuz?f{EAm|M!<9rskyxzV!51JBP_A4o^jz38ahm50xLV<&&%?1v`EKqy!WeH_c#2LbY)vx4)@>L|A~_Nu zM{ZSkx?BvWiUm247ip!9--@&~Tp7N`uIMk*KlAf5pT4Q>$rk9$`=ay4qdxdM7<#3p^QoG6p;&TMainCdQ(nb(^`S(;L+WG`s)Qv_*(3SyLXtquyA$L zj|`E`sPk27H9gv2jABXC6{CrHYqbpl(p!??zDKm94M{ZK@2YgQ>ywRFJT5Pge@c6b ze0jYzoG7KK$*&|#Y`X96rf#ikA#Lu;1Gqrw(!qU1Wf4??0S}FEBn`K9>lKGR0%Am} z;RheRy*#{<-8e$-(x7vzOg(urG)YP_`Lw~+IO6R)O-lw3lMM1W#=Rlpw7IgA8s^Xx z!4QxUZ7+ACO9lo8m{@d>x2|)}t#d@SbRxEAzT)7-Sm*FqDiQ#k%*Yxn^&WM&$?#b4 zQu`*B?e6lRQ({e2rarxJeNJ%c4|V@7x^mmNFHtyA_%JLy z{;9fVIdc*8xhuE)vZEM}^kY!^I8}c?k-seNZca-=i_u!4F-%*H2`Vfv|L%#idE%f+ zIT`|N|8)_WS~S(~--eOB8((7?Pk&TZ8JDR?_GjE}-S1zC&+B}|+nw9SjZqxgft`jm z!LcO`{`r%$aJotT{;GsQbp^xjJe6!(w4EN;J*meU`eV%0p6P_YJ{b@BorHr0g?W+& zpI)?et6XNwLTh-<9(2u!b_E9eu0u~S7Rn$y`u2|%Z;0t^xsgR!1E)9JkiQ7auFlIB z@4v1FdsYsl{hhXsU^k*aHD&%&Hk8zke4M!M(9o_FHLFpZt%8#|PLUK^``5TZv)~!O z20lYkfMgbYFOUVv&)u1R`UEX+Zp)VQg>9~NFAHAz|6UY&5~+UARLwgs{HyRkw?$vE z^2Cvm{vKgUXoz#Z4XZR0n4;=W4V|}o$1d}VS#q7hgT(pb1Cf+XP4Mjqo#=!&6I_Uh@id4ogI;EyOR|%{aEBOplE9s2)CNB*3R2`g8qX|e8&FD1DhmjB zeYjuHb~BwJm*u@=r>erW$P#+(++ct4S7|~8M3v5ASi2TbZKO*zSsO!~Cq;u6$d7N3 zs3ZTQ+2Z8QEOeX|Vka=Y$h`7MmFM+R1!e|P-8b*8MLI&hA7Fln zrlOK=Gb~g0!3Pp=XM!wE#Y6wCena%@6N-7%ouPNbw~mWE^3p!-NNp8wY}@<6p^x0B z4~=~rCph<)3|(~uZE2b5xA3#l`Giz^5nxgIg^@;aQ%Akp55A7buhfl*Djj_0L{2ni<&j!hnsK3n` zl2db~)pi)AI$iMcnH6Ogf2NaxY1ANHLh0ycnDJ4|e&z$wRZu2@!QK|(JzX!*`Wd+4 zvU>woN)^Drn>g_T8%$zy>{*Bp^8UvE(D8t&OU!$s+tY|RTvLmrW z>@Qz}mGtffO&ChxGh{lwuByBOI-nAMZYm_k4+#*>q$^|oihaO$fac<$d1z9R^ds-9 zpiGE9K|u5$PeL_{->XHfa_Jl{GVfYK?3(-2a_R^^$H`C=g`o%()gxJgORbsYPX1CT zC@i|1_stEM_hM-il3FC-u&t5xjD9WU0sUQ^{su|jcj)~`QHSJDHL}u3F5s4y{!^I`@0bABw~(JA_@dFKyIH^V&As1{Wr zxk4yyKK*Gvr>+nCA|X7?52jiN@bIqh2~>dYb$#F&aM^gWqH&yoekp(lF8(is9;vOJ zK2Km%>6gAw{If@q1!hN9`M(?KJTn=wE0kTIGk+%xHk%CU8O|Jyz6c5s zE_1kdIot?gZEj!kdBkND!%7b@t=ikr6|W_*OW2Kx=9$U$$uXE-K`k@CRUt!3d8mU%)X9eX0Bm=jOf6}=3iH> ze9p>vHmk^xj*N-P)ED~GMum1>p99`pIryVOFe!^bTWNN zNe$w)TeYVWUSq8oY{%oHg&I{qkk_Qf^S^jNs;w3zN}9xw_}x#U`l~9&SJ&6Ylg6?M zc}vM`FAm^ZqA0ry`#(f?ur?QFUZ&V8jaJ|bd7$fG%Qw%!9)01&wwKrXGsV7z-XAGh zOxlPdlId--syhr&LWF~{+0p^^cQp49zSD#<+eZT+za(EuVRY4aba$&pC}wM)d`c1WfD;)= zMfFWkLx1+%9}FACfeT57H$R5&c&6!-S$h;2wS8_53HY8)&3VRjqul6RCop;Tw>7!S zCo(?13wj9J+9)ag@ff|~e?3eqkavhAUytWCy^)%hmUuGD zYchEC3`jr?fg<))+Z|#FmYhkzKYYWbGA!~4GuU9YgqTfNqeG@kg0%d-w!)jGZ6?hA zXD`6aUUS<0t;_FmJ~rD2oVIpKtLofIi`iydS_3sP!poIg$UsVSKw{#|v;M=eSw!E` z(miqUdC2niDD3|Dh)yboChWEQ(u?=CJlF^FtuvU5om=PCf3mYVtR}jre3$AMA|krv zzjO`2-_^TNwvwAhQvK`-+kI={P0Y zruDkLc=3j6J=A%2d;8Vh-Cdo_e*LFC!IB6OqEBH&lOtxFp@bYLuC6>limF%CzuxGJ z%T_5MfbiNaV?jJFUqgPm9`d9oksJFRG2tw*%lCG#Ny3$WNf2q5^cskU=a{K)+egTk(1B&SAc)-gL7m^)#qU(=1-g?2KwB9R;eNZ02iwuTKf!z&WJGG6@LF zV;eqVZp*(`uBVzkum27nPX1LXdtgfF&)29xu(v-B2-L}P-iyDULfoFs5`cq~y6PUz zc(}hUb)J)h@Vo9FwcJu7iLU&#^9epcyV4HXl;64w{FsU%)ZKI+G<$Z5QEhb_^v$+e z8W6cBNeHMN=94F_HfXyGx9DA=gc87@MJE!*Y`Ho2d26u_g?v>lgz%9)V^m`)D>i)s zNgK6wv);B=%1)t!htnmdK5TAZW1*I$x;~Ah5RVHOB`;NMUY^-sl>*U)`YJLrJ%DT2 zers*g-+rB^vn2Qg@F%3I;qQ230Vcqq-Fka!BbT4_Al{l~@hN;dywHc)+Y#gX`uNo> zdol={>j4G8v~YQP%`A{(WMt50vAUu1oVA3AM8rG#Q%VgEp0S6Q{^;)wpcX(%t>@(S zJi&juGSAJ<5@uk`Qhrq4q*PEA7hWSp2Gz8wf^?P;4W9`YA08ZPAR8y zkb5Yj`NJ9wvs<>c)FNs8y}`WCgeDi0|eR%|1 zkTJeyarEX--_M^vJ4Z*c5fMF7+`@=z-mtKwGy6P#t8x7TWX+KslF#|df&W~L)<5Z* z9iu=ve6GF=d$|iod0_)GY)xT0Q4>r?UDUp$o0#Z>+gdFP4z?>-;uL-r&bKaZE0V6Rp zJ1cZl4|Um>ESMb2U)!^qtIr%M@&ZsU%&ZRe@_g0XN_8P3*fTvk6P)4FDbKaazIPHHz5(!%6=O zi2U#0i+}@oFzQV4zl%Y&9_Gb9=QLyYXReQ08V6`@ zzm#%cPyAg7aM|ZqDsq3$@4VZ3zPA!llxjM=5PrMnX&q=9Q=Z3oq-V&UG+XXWkSjWb zMV0|+xkgQ}_7j@kz6>&qq5{>o6H3`t<}@nH5X-8T=RhUh$HOd05q2yZ( z%68t`Adknd>D=R!epca=rZhf}bc1%Orz9SPdFZyj4>yqB=u2iYG{_?a@{Y~IZRtbA z-DFMrgRH2iXcyXpDLim*SaP!;-vE!Gd$Q_fRf7e) z@kwr_TOAjKRxb5fi_ak(|JBvWPC)*YGXmPHC*bkySvZ?#$198J;~a%Gd=Ql7VlmFG z0gawN!@@zS903@@O`wZ>j{k9Y(o!~&H!PlcP5~Ou(=HWiaMh&{1_vUMEJR!@`o9yYCDoY*+tvtebYj*;Rj>i{HBJ1h}@Y?$xK`VEE!PWO^7{SF# zD))P)mj>QYQ+M(&4G(8L-DvV1Jfm81=K05x(bjGP-+=+yrt3{a&E_*AaPeb{TNVHu zUc~fT>4=b%N6FL5W*hgrD4!KdfH>eYYucdCd5~WE>1z3c2|QM^q`!C3bZbMCpMUdg zuieEeDOh1)(hv8{iI$iOa0VTy|UF#ve+JW=EsBIDwAc2nM88mtnh7*69bJBmouPuE~IW=8|(&9|h zA-ItmTULgJJJML&I1GZPC6%}6|AH*6a4@oTvSFocBb?=D!hOBn&z#~94AgnL8DuDtOd5YxVgFOP#Ai-N)Ss(P1K`kj~h`c@z6v)WPvT5va6|=H62evr)E(`U#ARwY*!-j0HUJ1Fc3q^RmFWE2DI8EfU zd<_9x-KO@_1o7H-Rr*gR-8zHyiuMo?5RNwzmdf1EPoPcB0jm-KibflVE#(-8Y-g)S??=;l2w?ElJp>FMlQc=x^rNMjOPy1fT-IZc4;@gY^$s2F47UFlniaMye4nW zh09ZUmr<{n|I2bZ+-YwL8B}A4@T>c`5V0VPRanv*I7Ji`V6eDP?=LG(tk;T~8j+g4 z(}kclnz^JHXl+A7;h+vaD|NNKnDy^8G*BCAa^3q!=zFOOnSslLz56sBaRL|-EO!!(6YOu^%bHlbe`B6~(w)((h{-2A=9 zI?5VO914OM9J??b6S?<^7d>oACM`KDt7imUckPy(mk7_eF)%J->2;Eebp)Q-oz472 zEN(ALb?yrH!u0SEc!>hPCVU5Rc(&dZmI7Z)fa^i*a1(wA=KsHcC}fu_9&~Xiu>Jj4 z0-HjR@E4^0HA9i)&w=tdQM@m4dtnWSBpi4DZLCqt#rJUL8WXZLqyr$xYXHZSxvi~~ z(xFfRZ|{c}FJ5SB^|q4dTKE0c?u}8PT6^o~FWJjiyo)y~5`% zowX_P#k)9$IE@A~?j=v5xSk&37x1LCeDnh9l$5sfmx=Q`XFITpsID-g5dbfy*cS;o z-r4=#MX{e1NSegcEW!)o;O1XdCy{K0mU-6p!

$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/42] 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/42] 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; }