Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Iterable<StreamComponentBuilderExtension<Object>> streamChatComponentBuilders({
StreamComponentBuilder<MessageComposerInputHeaderProps>? messageComposerInputHeader,
StreamComponentBuilder<MessageComposerInputTrailingProps>? messageComposerInputTrailing,
StreamComponentBuilder<StreamMessageItemProps>? messageItem,
StreamComponentBuilder<StreamMessageLeadingProps>? messageLeading,
StreamComponentBuilder<StreamMessageHeaderProps>? messageHeader,
StreamComponentBuilder<StreamMessageFooterProps>? messageFooter,
StreamComponentBuilder<StreamMessageComposerAttachmentListProps>? messageComposerAttachmentList,
StreamComponentBuilder<StreamMessageComposerAttachmentProps>? messageComposerAttachment,

Expand Down Expand Up @@ -46,6 +49,9 @@ Iterable<StreamComponentBuilderExtension<Object>> 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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<StreamMessageFooterProps>();
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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<StreamMessageHeaderProps>();
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,
});
Expand All @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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.
///
/// 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.
class StreamMessageLeading extends core.NullableStatelessWidget {
/// Creates a message leading slot for the given [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<StreamMessageLeadingProps>();
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 = props.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),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +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/stream_chat_flutter.dart';
import 'package:stream_core_flutter/stream_core_flutter.dart' as core;

Expand Down Expand Up @@ -430,17 +428,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(
Expand Down Expand Up @@ -549,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,
Expand Down
3 changes: 3 additions & 0 deletions packages/stream_chat_flutter/lib/stream_chat_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading