From 15eb091c508592fe15047e8ffb1bcc3e37886e45 Mon Sep 17 00:00:00 2001 From: diego_bsit Date: Wed, 29 Oct 2025 11:28:59 +0100 Subject: [PATCH] feat(chat_message): add custom status and time builders to flyer_chat messages --- examples/flyer_chat/macos/Podfile.lock | 6 +- .../lib/src/flyer_chat_file_message.dart | 85 +++++++++++++----- .../lib/src/flyer_chat_image_message.dart | 87 +++++++++++++------ .../lib/src/flyer_chat_text_message.dart | 85 +++++++++++++----- .../src/flyer_chat_text_stream_message.dart | 85 +++++++++++++----- 5 files changed, 250 insertions(+), 98 deletions(-) diff --git a/examples/flyer_chat/macos/Podfile.lock b/examples/flyer_chat/macos/Podfile.lock index 9cc1e54c6..01e4869b1 100644 --- a/examples/flyer_chat/macos/Podfile.lock +++ b/examples/flyer_chat/macos/Podfile.lock @@ -36,11 +36,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 isar_flutter_libs: a65381780401f81ad6bf3f2e7cd0de5698fb98c4 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart b/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart index a1382e845..dc9d73eb1 100644 --- a/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart +++ b/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart @@ -77,6 +77,15 @@ class FlyerChatFileMessage extends StatelessWidget { /// The widget to display on top of the message. final Widget? topWidget; + /// Builder for the status widget if not provided, will build a default widget with Icons + /// Will not be displayed if [showStatus] is false. + final Widget Function(BuildContext context, MessageStatus? status)? + statusBuilder; + + /// Builder for the time widget, if not provided will build a default widget using the HH:mm format + /// Will not be displayed if [showTime] is false. + final Widget Function(BuildContext context, DateTime time)? timeBuilder; + /// Creates a widget to display a file message. const FlyerChatFileMessage({ super.key, @@ -96,7 +105,9 @@ class FlyerChatFileMessage extends StatelessWidget { this.receivedSizeTextStyle, this.timeStyle, this.showTime = true, + this.timeBuilder, this.showStatus = true, + this.statusBuilder, this.timeAndStatusPosition = TimeAndStatusPosition.end, this.topWidget, }); @@ -127,7 +138,9 @@ class FlyerChatFileMessage extends StatelessWidget { time: message.resolvedTime, status: message.resolvedStatus, showTime: showTime, + timeBuilder: timeBuilder, showStatus: isSentByMe && showStatus, + statusBuilder: statusBuilder, textStyle: timeStyle, ) : null; @@ -277,7 +290,7 @@ class FlyerChatFileMessage extends StatelessWidget { theme.bodySmall.copyWith(color: theme.onSurface.withValues(alpha: 0.8)); } - TextStyle? _resolveTimeStyle(bool isSentByMe, _LocalTheme theme) { + TextStyle _resolveTimeStyle(bool isSentByMe, _LocalTheme theme) { final color = isSentByMe ? theme.onPrimary : theme.onSurface; return timeStyle ?? theme.labelSmall.copyWith(color: color); @@ -299,41 +312,67 @@ class TimeAndStatus extends StatelessWidget { final bool showStatus; /// The text style for the time and status. - final TextStyle? textStyle; + final TextStyle textStyle; + + /// Builder for the status widget if not provided, will build a default widget with Icons + final Widget Function(BuildContext context, MessageStatus? status)? + statusBuilder; + + /// Builder for the time widget, if not provided will build a default widget using the HH:mm format + final Widget Function(BuildContext context, DateTime time)? timeBuilder; /// Creates a widget for displaying time and status. const TimeAndStatus({ super.key, required this.time, + required this.textStyle, this.status, this.showTime = true, this.showStatus = true, - this.textStyle, + this.timeBuilder, + this.statusBuilder, }); @override Widget build(BuildContext context) { + final timeWidgetBuilder = timeBuilder ?? _defaultTimeBuilder; + final statusWidgetBuilder = statusBuilder ?? _defaultStatusBuilder; + + return IconTheme( + data: IconThemeData(color: textStyle.color, size: 12), + child: DefaultTextStyle( + style: textStyle, + child: Row( + spacing: 2, + mainAxisSize: MainAxisSize.min, + children: [ + if (showTime && time != null) timeWidgetBuilder(context, time!), + if (showStatus && status != null) + statusWidgetBuilder(context, status), + ], + ), + ), + ); + } + + Widget _defaultTimeBuilder(BuildContext context, DateTime time) { final timeFormat = context.watch(); - return Row( - spacing: 2, - mainAxisSize: MainAxisSize.min, - children: [ - if (showTime && time != null) - Text(timeFormat.format(time!.toLocal()), style: textStyle), - if (showStatus && status != null) - if (status == MessageStatus.sending) - SizedBox( - width: 6, - height: 6, - child: CircularProgressIndicator( - color: textStyle?.color, - strokeWidth: 2, - ), - ) - else - Icon(getIconForStatus(status!), color: textStyle?.color, size: 12), - ], - ); + return Text(timeFormat.format(time.toLocal())); + } + + Widget _defaultStatusBuilder(BuildContext context, MessageStatus? status) { + if (status == MessageStatus.sending) { + return SizedBox( + width: 6, + height: 6, + child: CircularProgressIndicator( + color: textStyle.color, + strokeWidth: 2, + ), + ); + } + + return Icon(getIconForStatus(status!)); } } diff --git a/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart b/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart index 76414fe4b..af5e8b646 100644 --- a/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart +++ b/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart @@ -89,6 +89,15 @@ class FlyerChatImageMessage extends StatefulWidget { /// The widget to display on top of the message. final Widget? topWidget; + /// Builder for the status widget if not provided, will build a default widget with Icons + /// Will not be displayed if [showStatus] is false. + final Widget Function(BuildContext context, MessageStatus? status)? + statusBuilder; + + /// Builder for the time widget, if not provided will build a default widget using the HH:mm format + /// Will not be displayed if [showTime] is false. + final Widget Function(BuildContext context, DateTime time)? timeBuilder; + /// Creates a widget to display an image message. const FlyerChatImageMessage({ super.key, @@ -107,7 +116,9 @@ class FlyerChatImageMessage extends StatefulWidget { this.timeStyle, this.timeBackground, this.showTime = true, + this.timeBuilder, this.showStatus = true, + this.statusBuilder, this.timeAndStatusPosition = TimeAndStatusPosition.end, this.errorBuilder, this.topWidget, @@ -216,7 +227,9 @@ class _FlyerChatImageMessageState extends State time: widget.message.resolvedTime, status: widget.message.resolvedStatus, showTime: widget.showTime, + timeBuilder: widget.timeBuilder, showStatus: isSentByMe && widget.showStatus, + statusBuilder: widget.statusBuilder, backgroundColor: widget.timeBackground ?? Colors.black.withValues(alpha: 0.6), textStyle: @@ -409,22 +422,32 @@ class TimeAndStatus extends StatelessWidget { final Color? backgroundColor; /// Text style for the time and status. - final TextStyle? textStyle; + final TextStyle textStyle; + + /// Builder for the status widget if not provided, will build a default widget with Icons + final Widget Function(BuildContext context, MessageStatus? status)? + statusBuilder; + + /// Builder for the time widget, if not provided will build a default widget using the HH:mm format + final Widget Function(BuildContext context, DateTime time)? timeBuilder; /// Creates a widget for displaying time and status over an image. const TimeAndStatus({ super.key, required this.time, + required this.textStyle, this.status, this.showTime = true, + this.timeBuilder, this.showStatus = true, + this.statusBuilder, this.backgroundColor, - this.textStyle, }); @override Widget build(BuildContext context) { - final timeFormat = context.watch(); + final timeWidgetBuilder = timeBuilder ?? _defaultTimeBuilder; + final statusWidgetBuilder = statusBuilder ?? _defaultStatusBuilder; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -432,30 +455,42 @@ class TimeAndStatus extends StatelessWidget { color: backgroundColor, borderRadius: BorderRadius.circular(12), ), - child: Row( - spacing: 2, - mainAxisSize: MainAxisSize.min, - children: [ - if (showTime && time != null) - Text(timeFormat.format(time!.toLocal()), style: textStyle), - if (showStatus && status != null) - if (status == MessageStatus.sending) - SizedBox( - width: 6, - height: 6, - child: CircularProgressIndicator( - color: textStyle?.color, - strokeWidth: 2, - ), - ) - else - Icon( - getIconForStatus(status!), - color: textStyle?.color, - size: 12, - ), - ], + child: IconTheme( + data: IconThemeData(color: textStyle.color, size: 12), + child: DefaultTextStyle( + style: textStyle, + child: Row( + spacing: 2, + mainAxisSize: MainAxisSize.min, + children: [ + if (showTime && time != null) timeWidgetBuilder(context, time!), + if (showStatus && status != null) + statusWidgetBuilder(context, status), + ], + ), + ), ), ); } + + Widget _defaultTimeBuilder(BuildContext context, DateTime time) { + final timeFormat = context.watch(); + + return Text(timeFormat.format(time.toLocal())); + } + + Widget _defaultStatusBuilder(BuildContext context, MessageStatus? status) { + if (status == MessageStatus.sending) { + return SizedBox( + width: 6, + height: 6, + child: CircularProgressIndicator( + color: textStyle.color, + strokeWidth: 2, + ), + ); + } + + return Icon(getIconForStatus(status!)); + } } diff --git a/packages/flyer_chat_text_message/lib/src/flyer_chat_text_message.dart b/packages/flyer_chat_text_message/lib/src/flyer_chat_text_message.dart index 9b3d417e1..fd765ad08 100644 --- a/packages/flyer_chat_text_message/lib/src/flyer_chat_text_message.dart +++ b/packages/flyer_chat_text_message/lib/src/flyer_chat_text_message.dart @@ -81,6 +81,15 @@ class FlyerChatTextMessage extends StatelessWidget { /// The widget to display on top of the message. final Widget? topWidget; + /// Builder for the status widget if not provided, will build a default widget with Icons + /// Will not be displayed if [showStatus] is false. + final Widget Function(BuildContext context, MessageStatus? status)? + statusBuilder; + + /// Builder for the time widget, if not provided will build a default widget using the HH:mm format + /// Will not be displayed if [showTime] is false. + final Widget Function(BuildContext context, DateTime time)? timeBuilder; + /// Creates a widget to display a text message. const FlyerChatTextMessage({ super.key, @@ -98,7 +107,9 @@ class FlyerChatTextMessage extends StatelessWidget { this.receivedLinksColor, this.timeStyle, this.showTime = true, + this.timeBuilder, this.showStatus = true, + this.statusBuilder, this.timeAndStatusPosition = TimeAndStatusPosition.end, this.timeAndStatusPositionInlineInsets = const EdgeInsets.only(bottom: 2), this.onLinkTap, @@ -131,7 +142,9 @@ class FlyerChatTextMessage extends StatelessWidget { ? TimeAndStatus( time: message.resolvedTime, status: message.resolvedStatus, + statusBuilder: statusBuilder, showTime: showTime, + timeBuilder: timeBuilder, showStatus: isSentByMe && showStatus, textStyle: timeStyle, ) @@ -266,7 +279,7 @@ class FlyerChatTextMessage extends StatelessWidget { theme.bodyMedium.copyWith(color: theme.onSurface); } - TextStyle? _resolveTimeStyle(bool isSentByMe, _LocalTheme theme) { + TextStyle _resolveTimeStyle(bool isSentByMe, _LocalTheme theme) { if (isSentByMe) { return timeStyle ?? theme.labelSmall.copyWith( @@ -292,41 +305,67 @@ class TimeAndStatus extends StatelessWidget { final bool showStatus; /// The text style for the time and status. - final TextStyle? textStyle; + final TextStyle textStyle; + + /// Builder for the status widget if not provided, will build a default widget with Icons + final Widget Function(BuildContext context, MessageStatus? status)? + statusBuilder; + + /// Builder for the time widget, if not provided will build a default widget using the HH:mm format + final Widget Function(BuildContext context, DateTime time)? timeBuilder; /// Creates a widget for displaying time and status. const TimeAndStatus({ super.key, required this.time, + required this.textStyle, this.status, this.showTime = true, this.showStatus = true, - this.textStyle, + this.timeBuilder, + this.statusBuilder, }); @override Widget build(BuildContext context) { + final timeWidgetBuilder = timeBuilder ?? _defaultTimeBuilder; + final statusWidgetBuilder = statusBuilder ?? _defaultStatusBuilder; + + return IconTheme( + data: IconThemeData(color: textStyle.color, size: 12), + child: DefaultTextStyle( + style: textStyle, + child: Row( + spacing: 2, + mainAxisSize: MainAxisSize.min, + children: [ + if (showTime && time != null) timeWidgetBuilder(context, time!), + if (showStatus && status != null) + statusWidgetBuilder(context, status), + ], + ), + ), + ); + } + + Widget _defaultTimeBuilder(BuildContext context, DateTime time) { final timeFormat = context.watch(); - return Row( - spacing: 2, - mainAxisSize: MainAxisSize.min, - children: [ - if (showTime && time != null) - Text(timeFormat.format(time!.toLocal()), style: textStyle), - if (showStatus && status != null) - if (status == MessageStatus.sending) - SizedBox( - width: 6, - height: 6, - child: CircularProgressIndicator( - color: textStyle?.color, - strokeWidth: 2, - ), - ) - else - Icon(getIconForStatus(status!), color: textStyle?.color, size: 12), - ], - ); + return Text(timeFormat.format(time.toLocal())); + } + + Widget _defaultStatusBuilder(BuildContext context, MessageStatus? status) { + if (status == MessageStatus.sending) { + return SizedBox( + width: 6, + height: 6, + child: CircularProgressIndicator( + color: textStyle.color, + strokeWidth: 2, + ), + ); + } + + return Icon(getIconForStatus(status!)); } } diff --git a/packages/flyer_chat_text_stream_message/lib/src/flyer_chat_text_stream_message.dart b/packages/flyer_chat_text_stream_message/lib/src/flyer_chat_text_stream_message.dart index deab4d099..4d1514561 100644 --- a/packages/flyer_chat_text_stream_message/lib/src/flyer_chat_text_stream_message.dart +++ b/packages/flyer_chat_text_stream_message/lib/src/flyer_chat_text_stream_message.dart @@ -104,6 +104,15 @@ class FlyerChatTextStreamMessage extends StatefulWidget { /// The period of the shimmer loading animation. final Duration shimmerPeriod; + /// Builder for the status widget if not provided, will build a default widget with Icons + /// Will not be displayed if [showStatus] is false. + final Widget Function(BuildContext context, MessageStatus? status)? + statusBuilder; + + /// Builder for the time widget, if not provided will build a default widget using the HH:mm format + /// Will not be displayed if [showTime] is false. + final Widget Function(BuildContext context, DateTime time)? timeBuilder; + /// A builder to completely override the default loading widget. /// If provided, `loadingText`, `shimmerBaseColor`, and `shimmerHighlightColor` are ignored. final Widget Function(BuildContext context, TextStyle? paragraphStyle)? @@ -123,7 +132,9 @@ class FlyerChatTextStreamMessage extends StatefulWidget { this.receivedTextStyle, this.timeStyle, this.showTime = true, + this.timeBuilder, this.showStatus = true, + this.statusBuilder, this.timeAndStatusPosition = TimeAndStatusPosition.end, this.chunkAnimationDuration = const Duration(milliseconds: 350), this.mode = TextStreamMessageMode.animatedOpacity, @@ -318,6 +329,8 @@ class _FlyerChatTextStreamMessageState extends State showTime: widget.showTime, showStatus: isSentByMe && widget.showStatus, textStyle: timeStyle, + timeBuilder: widget.timeBuilder, + statusBuilder: widget.statusBuilder, ) : null; @@ -484,7 +497,7 @@ class _FlyerChatTextStreamMessageState extends State theme.bodyMedium.copyWith(color: theme.onSurface); } - TextStyle? _resolveTimeStyle(bool isSentByMe, _LocalTheme theme) { + TextStyle _resolveTimeStyle(bool isSentByMe, _LocalTheme theme) { if (isSentByMe) { return widget.timeStyle ?? theme.labelSmall.copyWith(color: theme.onPrimary); @@ -515,41 +528,67 @@ class TimeAndStatus extends StatelessWidget { final bool showStatus; /// The text style for the time and status. - final TextStyle? textStyle; + final TextStyle textStyle; + + /// Builder for the status widget if not provided, will build a default widget with Icons + final Widget Function(BuildContext context, MessageStatus? status)? + statusBuilder; + + /// Builder for the time widget, if not provided will build a default widget using the HH:mm format + final Widget Function(BuildContext context, DateTime time)? timeBuilder; /// Creates a widget for displaying time and status. const TimeAndStatus({ super.key, required this.time, + required this.textStyle, this.status, this.showTime = true, this.showStatus = true, - this.textStyle, + this.statusBuilder, + this.timeBuilder, }); @override Widget build(BuildContext context) { + final timeWidgetBuilder = timeBuilder ?? _defaultTimeBuilder; + final statusWidgetBuilder = statusBuilder ?? _defaultStatusBuilder; + + return IconTheme( + data: IconThemeData(color: textStyle.color, size: 12), + child: DefaultTextStyle( + style: textStyle, + child: Row( + spacing: 2, + mainAxisSize: MainAxisSize.min, + children: [ + if (showTime && time != null) timeWidgetBuilder(context, time!), + if (showStatus && status != null) + statusWidgetBuilder(context, status), + ], + ), + ), + ); + } + + Widget _defaultTimeBuilder(BuildContext context, DateTime time) { final timeFormat = context.watch(); - return Row( - spacing: 2, - mainAxisSize: MainAxisSize.min, - children: [ - if (showTime && time != null) - Text(timeFormat.format(time!.toLocal()), style: textStyle), - if (showStatus && status != null) - if (status == MessageStatus.sending) - SizedBox( - width: 6, - height: 6, - child: CircularProgressIndicator( - color: textStyle?.color, - strokeWidth: 2, - ), - ) - else - Icon(getIconForStatus(status!), color: textStyle?.color, size: 12), - ], - ); + return Text(timeFormat.format(time.toLocal())); + } + + Widget _defaultStatusBuilder(BuildContext context, MessageStatus? status) { + if (status == MessageStatus.sending) { + return SizedBox( + width: 6, + height: 6, + child: CircularProgressIndicator( + color: textStyle.color, + strokeWidth: 2, + ), + ); + } + + return Icon(getIconForStatus(status!)); } }