From 902a1fbf7687ac65a0e33622f537594ff9fa7bd9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 Jun 2026 16:00:45 +0200 Subject: [PATCH 1/3] refactor(ui): extract StreamMessageLeading from message item Move the leading avatar rendering out of DefaultStreamMessageItem.build and into its own NullableStatelessWidget under components/, mirroring the structure already used by StreamMessageHeader and StreamMessageFooter. Behavior is unchanged; the three slots are now composed symmetrically. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/stream_chat_flutter/CHANGELOG.md | 1 + .../components/stream_message_leading.dart | 36 +++++++++++++++++++ .../message_widget/stream_message_item.dart | 15 +++----- 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_leading.dart diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index e3e6453f6..f64a6a2a6 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -18,6 +18,7 @@ - Composer UI primitives (`StreamMessageComposerInputField`, `VoiceRecordingCallback`, and the outer/inner layout containers) are now owned by `stream_chat_flutter` and exported from this package. They were previously supplied by `stream_core_flutter`. The public API of `StreamMessageComposer` / `StreamChatMessageInput` and its sub-components is unchanged. - Re-export `StreamAvatarTheme` and `StreamAvatarThemeData` from `stream_core_flutter` so consumers can theme avatars without adding a separate `stream_core_flutter` import. +- Extracted the message item's leading avatar rendering into a new internal `StreamMessageLeading` widget, matching the structure used by `StreamMessageHeader` and `StreamMessageFooter`. Behavior is unchanged; `DefaultStreamMessageItem` now composes leading, header, and footer symmetrically. 🛑️ Breaking diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_leading.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_leading.dart new file mode 100644 index 000000000..be0606034 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_leading.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// Displays the leading slot of a message item — by default the author's +/// avatar shown to the side of the message bubble. +/// +/// Returns `null` when [Message.user] is null, allowing the parent layout to +/// collapse the slot and skip spacing automatically. +/// +/// See also: +/// +/// * [StreamMessageHeader], the annotation slot above the bubble. +/// * [StreamMessageFooter], the metadata slot below the bubble. +/// * [DefaultStreamMessageItem], which controls leading visibility. +class StreamMessageLeading extends core.NullableStatelessWidget { + /// Creates a message leading slot for the given [message]. + const StreamMessageLeading({super.key, required this.message}); + + /// The message whose author avatar to display. + final Message message; + + @override + Widget? nullableBuild(BuildContext context) { + final user = message.user; + if (user == null) return null; + + final theme = core.StreamMessageItemTheme.of(context); + final avatarSize = theme.avatarSize ?? StreamAvatarSize.md; + + return core.StreamAvatarTheme( + data: .new(size: avatarSize), + child: StreamUserAvatar(user: user, showOnlineIndicator: false), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart b/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart index 34d781f4e..ebcaf0d51 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart @@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/context_menu/context_menu_region.dart'; import 'package:stream_chat_flutter/src/message_widget/components/stream_message_content.dart'; import 'package:stream_chat_flutter/src/message_widget/components/stream_message_footer.dart'; import 'package:stream_chat_flutter/src/message_widget/components/stream_message_header.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_leading.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_core_flutter/stream_core_flutter.dart' as core; @@ -430,17 +431,9 @@ class DefaultStreamMessageItem extends StatelessWidget { final effectiveMetadataVisibility = resolve((theme) => theme?.metadataVisibility); final effectiveRepliesVisibility = resolve((theme) => theme?.repliesVisibility); - Widget? leadingWidget; - if (props.message.user case final user?) { - final effectiveAvatarSize = theme.avatarSize ?? defaults.avatarSize; - - leadingWidget = effectiveAvatarVisibility.apply( - core.StreamAvatarTheme( - data: .new(size: effectiveAvatarSize), - child: StreamUserAvatar(user: user, showOnlineIndicator: false), - ), - ); - } + final leadingWidget = effectiveAvatarVisibility.apply( + StreamMessageLeading(message: message), + ); final headerWidget = effectiveAnnotationVisibility.apply( StreamMessageHeader( From b2d686ed3715b500f8e676a9fca571608ad08d97 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 Jun 2026 16:42:20 +0200 Subject: [PATCH 2/3] feat(ui): factory slots for message item leading, header, footer Register messageLeading, messageHeader, and messageFooter in streamChatComponentBuilders so integrators can override each sub-component independently instead of replacing the whole messageItem. Each slot follows the same shape as StreamMessageItem: a public widget that resolves chatComponentBuilder<...Props>(), a Props data class, and a DefaultStream... implementation holding the rendering. The widgets are now exported from the package barrel. Closes a gap with iOS (makeUserAvatarView), Android Compose (MessageAuthor / MessageTop / MessageBottom) and React Native (MessageAuthor / MessageHeader / MessageFooter). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/stream_chat_flutter/CHANGELOG.md | 2 +- .../stream_chat_component_builders.dart | 6 ++ .../components/stream_message_footer.dart | 73 +++++++++++--- .../components/stream_message_header.dart | 98 +++++++++++++++---- .../components/stream_message_leading.dart | 61 +++++++++++- .../message_widget/stream_message_item.dart | 3 - .../lib/stream_chat_flutter.dart | 3 + 7 files changed, 202 insertions(+), 44 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index f64a6a2a6..1641f6bb4 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -18,7 +18,7 @@ - Composer UI primitives (`StreamMessageComposerInputField`, `VoiceRecordingCallback`, and the outer/inner layout containers) are now owned by `stream_chat_flutter` and exported from this package. They were previously supplied by `stream_core_flutter`. The public API of `StreamMessageComposer` / `StreamChatMessageInput` and its sub-components is unchanged. - Re-export `StreamAvatarTheme` and `StreamAvatarThemeData` from `stream_core_flutter` so consumers can theme avatars without adding a separate `stream_core_flutter` import. -- Extracted the message item's leading avatar rendering into a new internal `StreamMessageLeading` widget, matching the structure used by `StreamMessageHeader` and `StreamMessageFooter`. Behavior is unchanged; `DefaultStreamMessageItem` now composes leading, header, and footer symmetrically. +- Added `messageLeading`, `messageHeader`, and `messageFooter` factory slots to `streamChatComponentBuilders` for overriding the message item's avatar, annotations, or metadata row without replacing the whole `messageItem`. 🛑️ Breaking diff --git a/packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart b/packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart index 4994d48b0..5c06c0673 100644 --- a/packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart +++ b/packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart @@ -17,6 +17,9 @@ Iterable> streamChatComponentBuilders({ StreamComponentBuilder? messageComposerInputHeader, StreamComponentBuilder? messageComposerInputTrailing, StreamComponentBuilder? messageItem, + StreamComponentBuilder? messageLeading, + StreamComponentBuilder? messageHeader, + StreamComponentBuilder? messageFooter, StreamComponentBuilder? messageComposerAttachmentList, StreamComponentBuilder? messageComposerAttachment, @@ -46,6 +49,9 @@ Iterable> streamChatComponentBuilders({ if (messageComposerInputHeader != null) StreamComponentBuilderExtension(builder: messageComposerInputHeader), if (messageComposerInputTrailing != null) StreamComponentBuilderExtension(builder: messageComposerInputTrailing), if (messageItem != null) StreamComponentBuilderExtension(builder: messageItem), + if (messageLeading != null) StreamComponentBuilderExtension(builder: messageLeading), + if (messageHeader != null) StreamComponentBuilderExtension(builder: messageHeader), + if (messageFooter != null) StreamComponentBuilderExtension(builder: messageFooter), if (messageComposerAttachmentList != null) StreamComponentBuilderExtension(builder: messageComposerAttachmentList), if (messageComposerAttachment != null) StreamComponentBuilderExtension(builder: messageComposerAttachment), if (imageAttachment != null) StreamComponentBuilderExtension(builder: imageAttachment), diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_footer.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_footer.dart index 1e6746dd2..20d5227aa 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_footer.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_footer.dart @@ -1,37 +1,80 @@ import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/message_widget/components/stream_message_sending_status.dart'; -import 'package:stream_chat_flutter/src/misc/timestamp.dart'; -import 'package:stream_chat_flutter/src/stream_chat.dart'; -import 'package:stream_chat_flutter/src/utils/extensions.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_core_flutter/stream_core_flutter.dart' as core; /// Displays the row below the message bubble containing the author name, /// sending status, creation timestamp, and an edited indicator. /// -/// The footer can show up to four pieces depending on the message: -/// -/// * **Username** — for messages from other users. -/// * **Sending status** — for the current user's own messages. -/// * **Timestamp** — always shown, formatted as a short time string. -/// * **Edited label** — when the message text has been updated. +/// This widget delegates rendering to either a custom builder registered via +/// [StreamComponentFactory], or [DefaultStreamMessageFooter] when no custom +/// builder is provided. Register a custom builder through +/// `streamChatComponentBuilders(messageFooter: ...)` to fully replace the +/// default footer rendering while still receiving the same +/// [StreamMessageFooterProps]. /// /// See also: /// +/// * [StreamMessageFooterProps], which holds every configurable property. +/// * [DefaultStreamMessageFooter], the default implementation used when no +/// custom builder is registered. /// * [StreamMessageHeader], the symmetric slot above the message bubble. -/// * [StreamMessageSendingStatus], which renders the sent/delivered/read -/// indicator. -/// * [DefaultStreamMessageItem], which controls footer visibility. class StreamMessageFooter extends StatelessWidget { /// Creates a message footer for the given [message]. - const StreamMessageFooter({super.key, required this.message}); + StreamMessageFooter({super.key, required Message message}) : props = .new(message: message); + + /// Creates a message footer from pre-built [props]. + const StreamMessageFooter.fromProps({super.key, required this.props}); + + /// The properties that configure this footer. + final StreamMessageFooterProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamMessageFooter(props: props); + } +} + +/// Properties for configuring a [StreamMessageFooter]. +/// +/// See also: +/// +/// * [StreamMessageFooter], which uses these properties. +/// * [DefaultStreamMessageFooter], the default implementation. +class StreamMessageFooterProps { + /// Creates properties for a message footer. + const StreamMessageFooterProps({required this.message}); /// The message whose metadata to display. final Message message; + /// Returns a copy of this [StreamMessageFooterProps] with the given fields + /// replaced with new values. + StreamMessageFooterProps copyWith({Message? message}) { + return StreamMessageFooterProps(message: message ?? this.message); + } +} + +/// The default implementation of [StreamMessageFooter]. +/// +/// The footer can show up to four pieces depending on the message: +/// +/// * **Username** — for messages from other users. +/// * **Sending status** — for the current user's own messages. +/// * **Timestamp** — always shown, formatted as a short time string. +/// * **Edited label** — when the message text has been updated. +class DefaultStreamMessageFooter extends StatelessWidget { + /// Creates a default message footer with the given [props]. + const DefaultStreamMessageFooter({super.key, required this.props}); + + /// The properties that configure this widget. + final StreamMessageFooterProps props; + @override Widget build(BuildContext context) { + final message = props.message; final currentUser = StreamChat.of(context).currentUser; final channelKind = core.StreamMessageLayout.channelKindOf(context); diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_header.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_header.dart index 1f0b7d871..4fa94ebdf 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_header.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_header.dart @@ -1,34 +1,57 @@ import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/src/stream_chat.dart'; -import 'package:stream_chat_flutter/src/utils/extensions.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_core_flutter/stream_core_flutter.dart' as core; /// Displays contextual annotations above the message bubble for the given -/// [message]. +/// message. /// -/// Annotations are shown in the following order when applicable: -/// -/// 1. **Saved for later** — when a reminder exists without a scheduled time. -/// 2. **Pinned** — when [Message.pinned] is true, showing who pinned it. -/// 3. **Show in channel / Replied to thread** — when [Message.showInChannel] -/// is true. The label adapts based on whether the message list is a -/// channel or thread view, and includes a tappable "View" link that -/// invokes [onViewChannelTap]. -/// 4. **Reminder** — when a reminder exists with a scheduled time. -/// -/// Returns `null` when no annotations apply, allowing [StreamColumn] to -/// collapse the widget and skip spacing automatically. +/// This widget delegates rendering to either a custom builder registered via +/// [StreamComponentFactory], or [DefaultStreamMessageHeader] when no custom +/// builder is provided. Register a custom builder through +/// `streamChatComponentBuilders(messageHeader: ...)` to fully replace the +/// default header rendering while still receiving the same +/// [StreamMessageHeaderProps]. /// /// See also: /// +/// * [StreamMessageHeaderProps], which holds every configurable property. +/// * [DefaultStreamMessageHeader], the default implementation used when no +/// custom builder is registered. /// * [StreamMessageFooter], the symmetric slot below the message bubble. -/// * [DefaultStreamMessageItem], which controls header visibility. class StreamMessageHeader extends core.NullableStatelessWidget { /// Creates a message header for the given [message]. - const StreamMessageHeader({ + StreamMessageHeader({ super.key, + required Message message, + VoidCallback? onViewChannelTap, + }) : props = .new( + message: message, + onViewChannelTap: onViewChannelTap, + ); + + /// Creates a message header from pre-built [props]. + const StreamMessageHeader.fromProps({super.key, required this.props}); + + /// The properties that configure this header. + final StreamMessageHeaderProps props; + + @override + Widget? nullableBuild(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamMessageHeader(props: props); + } +} + +/// Properties for configuring a [StreamMessageHeader]. +/// +/// See also: +/// +/// * [StreamMessageHeader], which uses these properties. +/// * [DefaultStreamMessageHeader], the default implementation. +class StreamMessageHeaderProps { + /// Creates properties for a message header. + const StreamMessageHeaderProps({ required this.message, this.onViewChannelTap, }); @@ -39,8 +62,43 @@ class StreamMessageHeader extends core.NullableStatelessWidget { /// Called when the "View" link in the show-in-channel annotation is tapped. final VoidCallback? onViewChannelTap; + /// Returns a copy of this [StreamMessageHeaderProps] with the given fields + /// replaced with new values. + StreamMessageHeaderProps copyWith({ + Message? message, + VoidCallback? onViewChannelTap, + }) { + return StreamMessageHeaderProps( + message: message ?? this.message, + onViewChannelTap: onViewChannelTap ?? this.onViewChannelTap, + ); + } +} + +/// The default implementation of [StreamMessageHeader]. +/// +/// Annotations are shown in the following order when applicable: +/// +/// 1. **Saved for later** — when a reminder exists without a scheduled time. +/// 2. **Pinned** — when [Message.pinned] is true, showing who pinned it. +/// 3. **Show in channel / Replied to thread** — when [Message.showInChannel] +/// is true. The label adapts based on whether the message list is a +/// channel or thread view, and includes a tappable "View" link that +/// invokes [StreamMessageHeaderProps.onViewChannelTap]. +/// 4. **Reminder** — when a reminder exists with a scheduled time. +/// +/// Returns `null` when no annotations apply, allowing the parent layout to +/// collapse the slot and skip spacing automatically. +class DefaultStreamMessageHeader extends core.NullableStatelessWidget { + /// Creates a default message header with the given [props]. + const DefaultStreamMessageHeader({super.key, required this.props}); + + /// The properties that configure this widget. + final StreamMessageHeaderProps props; + @override Widget? nullableBuild(BuildContext context) { + final message = props.message; final translations = context.translations; final icons = context.streamIcons; final colorScheme = context.streamColorScheme; @@ -75,7 +133,7 @@ class StreamMessageHeader extends core.NullableStatelessWidget { }; showInChannelAnnotation = core.StreamMessageAnnotation( - onTap: onViewChannelTap, + onTap: props.onViewChannelTap, leading: Icon(icons.arrowUpRight), label: Text(annotationLabel), trailing: Text(translations.viewLabel), diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_leading.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_leading.dart index be0606034..e36a0e036 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_leading.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_leading.dart @@ -5,24 +5,75 @@ import 'package:stream_core_flutter/stream_core_flutter.dart' as core; /// Displays the leading slot of a message item — by default the author's /// avatar shown to the side of the message bubble. /// -/// Returns `null` when [Message.user] is null, allowing the parent layout to -/// collapse the slot and skip spacing automatically. +/// This widget delegates rendering to either a custom builder registered via +/// [StreamComponentFactory], or [DefaultStreamMessageLeading] when no custom +/// builder is provided. Register a custom builder through +/// `streamChatComponentBuilders(messageLeading: ...)` to fully replace the +/// default leading rendering while still receiving the same +/// [StreamMessageLeadingProps]. /// /// See also: /// +/// * [StreamMessageLeadingProps], which holds every configurable property. +/// * [DefaultStreamMessageLeading], the default implementation used when no +/// custom builder is registered. /// * [StreamMessageHeader], the annotation slot above the bubble. /// * [StreamMessageFooter], the metadata slot below the bubble. -/// * [DefaultStreamMessageItem], which controls leading visibility. class StreamMessageLeading extends core.NullableStatelessWidget { /// Creates a message leading slot for the given [message]. - const StreamMessageLeading({super.key, required this.message}); + StreamMessageLeading({super.key, required Message message}) : props = .new(message: message); + + /// Creates a message leading slot from pre-built [props]. + const StreamMessageLeading.fromProps({super.key, required this.props}); + + /// The properties that configure this leading slot. + final StreamMessageLeadingProps props; + + @override + Widget? nullableBuild(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamMessageLeading(props: props); + } +} + +/// Properties for configuring a [StreamMessageLeading]. +/// +/// See also: +/// +/// * [StreamMessageLeading], which uses these properties. +/// * [DefaultStreamMessageLeading], the default implementation. +class StreamMessageLeadingProps { + /// Creates properties for a message leading slot. + const StreamMessageLeadingProps({required this.message}); /// The message whose author avatar to display. final Message message; + /// Returns a copy of this [StreamMessageLeadingProps] with the given fields + /// replaced with new values. + StreamMessageLeadingProps copyWith({Message? message}) { + return StreamMessageLeadingProps(message: message ?? this.message); + } +} + +/// The default implementation of [StreamMessageLeading]. +/// +/// Renders the author's avatar at the size configured by +/// [core.StreamMessageItemTheme.avatarSize], falling back to +/// [StreamAvatarSize.md]. Returns `null` when [Message.user] is null, +/// allowing the parent layout to collapse the slot and skip spacing +/// automatically. +class DefaultStreamMessageLeading extends core.NullableStatelessWidget { + /// Creates a default message leading slot with the given [props]. + const DefaultStreamMessageLeading({super.key, required this.props}); + + /// The properties that configure this widget. + final StreamMessageLeadingProps props; + @override Widget? nullableBuild(BuildContext context) { - final user = message.user; + final user = props.message.user; if (user == null) return null; final theme = core.StreamMessageItemTheme.of(context); diff --git a/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart b/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart index ebcaf0d51..d05750669 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart @@ -7,9 +7,6 @@ import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_ import 'package:stream_chat_flutter/src/context_menu/context_menu.dart'; import 'package:stream_chat_flutter/src/context_menu/context_menu_region.dart'; import 'package:stream_chat_flutter/src/message_widget/components/stream_message_content.dart'; -import 'package:stream_chat_flutter/src/message_widget/components/stream_message_footer.dart'; -import 'package:stream_chat_flutter/src/message_widget/components/stream_message_header.dart'; -import 'package:stream_chat_flutter/src/message_widget/components/stream_message_leading.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_core_flutter/stream_core_flutter.dart' as core; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 217cc4121..2e1154ffe 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -203,6 +203,9 @@ export 'src/message_modal/message_action_confirmation_modal.dart'; export 'src/message_modal/message_actions_modal.dart'; export 'src/message_modal/message_modal.dart'; export 'src/message_modal/moderated_message_actions_modal.dart'; +export 'src/message_widget/components/stream_message_footer.dart'; +export 'src/message_widget/components/stream_message_header.dart'; +export 'src/message_widget/components/stream_message_leading.dart'; export 'src/message_widget/stream_message_item.dart'; export 'src/message_widget/stream_moderated_message.dart'; export 'src/message_widget/stream_quoted_message.dart'; From 2e82c375613b245355b25b070b72a5e615325253 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 Jun 2026 17:02:36 +0200 Subject: [PATCH 3/3] fix(ui): use StreamRow in message item so collapsed leading skips spacing When the leading slot's nullableBuild returns null (e.g. message.user is null) the framework substitutes _CollapsedWidget. A regular Row counts this as a child and applies its spacing, leaving an extra gap before the content column. StreamRow skips _RenderCollapsed children, matching the StreamColumn already used inside StreamMessageContent for header and footer. Defers the visibility decision to the theme: if a theme resolves the avatar to .visible while message.user is null, the slot renders an empty widget without disturbing the row's horizontal rhythm. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lib/src/message_widget/stream_message_item.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart b/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart index d05750669..da1febabe 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dart @@ -539,7 +539,7 @@ class DefaultStreamMessageItem extends StatelessWidget { alignment: StreamMessageLayout.alignmentDirectionalOf(context), child: Padding( padding: effectivePadding, - child: Row( + child: core.StreamRow( mainAxisSize: .min, spacing: effectiveSpacing, crossAxisAlignment: .end,