diff --git a/examples/flyer_chat/lib/local.dart b/examples/flyer_chat/lib/local.dart index ab1e5dd3..0a3b0189 100644 --- a/examples/flyer_chat/lib/local.dart +++ b/examples/flyer_chat/lib/local.dart @@ -25,8 +25,14 @@ class LocalState extends State { final _chatController = InMemoryChatController(); final _uuid = const Uuid(); - final _currentUser = const User(id: 'me'); - final _recipient = const User(id: 'recipient'); + final _currentUser = const User( + id: 'me', + imageSource: 'https://picsum.photos/id/65/200/200', + ); + final _recipient = const User( + id: 'recipient', + imageSource: 'https://picsum.photos/id/265/200/200', + ); final _systemUser = const User(id: 'system'); bool _isTyping = false; @@ -87,6 +93,39 @@ class LocalState extends State { ), textMessageBuilder: (context, message, index) => FlyerChatTextMessage(message: message, index: index), + chatMessageBuilder: ( + context, + message, + index, + animation, + child, { + bool? isRemoved, + MessageGroupStatus? groupStatus, + }) { + return ChatMessage( + message: message, + index: index, + animation: animation, + groupStatus: groupStatus, + leadingWidget: message.authorId != _currentUser.id && + (groupStatus?.isLast ?? true) && + isRemoved != true + ? Padding( + padding: const EdgeInsets.only(right: 8), + child: Avatar(userId: message.authorId), + ) + : const SizedBox(width: 40), + trailingWidget: message.authorId == _currentUser.id && + (groupStatus?.isLast ?? true) && + isRemoved != true + ? Padding( + padding: const EdgeInsets.only(left: 8), + child: Avatar(userId: message.authorId), + ) + : const SizedBox(width: 40), + child: child, + ); + }, ), chatController: _chatController, currentUserId: _currentUser.id, diff --git a/examples/flyer_chat/pubspec.yaml b/examples/flyer_chat/pubspec.yaml index 0b17d12e..d2189bdf 100644 --- a/examples/flyer_chat/pubspec.yaml +++ b/examples/flyer_chat/pubspec.yaml @@ -30,17 +30,17 @@ environment: dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cross_cache: ^0.0.5 + cross_cache: ^0.0.6 cupertino_icons: ^1.0.8 dio: ^5.7.0 flutter: sdk: flutter - flutter_chat_core: ^0.0.5 - flutter_chat_ui: ^2.0.0-dev.4 + flutter_chat_core: ^0.0.6 + flutter_chat_ui: ^2.0.0-dev.5 flutter_dotenv: ^5.2.1 flutter_lorem: ^2.0.0 - flyer_chat_image_message: ^0.0.5 - flyer_chat_text_message: ^0.0.5 + flyer_chat_image_message: ^0.0.6 + flyer_chat_text_message: ^0.0.6 google_generative_ai: ^0.4.6 hive: ^4.0.0-dev.2 http: ^1.2.2 diff --git a/packages/cross_cache/CHANGELOG.md b/packages/cross_cache/CHANGELOG.md index edc6c48d..c1bde010 100644 --- a/packages/cross_cache/CHANGELOG.md +++ b/packages/cross_cache/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Version bump to match other packages diff --git a/packages/cross_cache/pubspec.yaml b/packages/cross_cache/pubspec.yaml index aecbadbe..0b56c2ed 100644 --- a/packages/cross_cache/pubspec.yaml +++ b/packages/cross_cache/pubspec.yaml @@ -1,5 +1,5 @@ name: cross_cache -version: 0.0.5 +version: 0.0.6 description: > Cross-platform cache manager for Flutter using IndexedDB for web, file system for mobile and desktop, and Dio for network requests. #cache #indexeddb #dio diff --git a/packages/flutter_chat_core/CHANGELOG.md b/packages/flutter_chat_core/CHANGELOG.md index 24514a5d..7833162a 100644 --- a/packages/flutter_chat_core/CHANGELOG.md +++ b/packages/flutter_chat_core/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.0.6 + +**⚠️ Breaking changes ⚠️** + +- Changed signature of `chatMessageBuilder` to include `isRemoved` and `groupStatus` parameters. +- Changed `imageUrl` to `imageSource` for the `User` model. Change is necessary to show that not only remote URLs are supported but also local assets. + ## 0.0.5 **⚠️ Breaking changes ⚠️** diff --git a/packages/flutter_chat_core/lib/flutter_chat_core.dart b/packages/flutter_chat_core/lib/flutter_chat_core.dart index 1e9eab0d..e0657955 100644 --- a/packages/flutter_chat_core/lib/flutter_chat_core.dart +++ b/packages/flutter_chat_core/lib/flutter_chat_core.dart @@ -5,6 +5,7 @@ export 'src/chat_controller/upload_progress_mixin.dart'; export 'src/models/builders.dart'; export 'src/models/link_preview.dart'; export 'src/models/message.dart'; +export 'src/models/message_group_status.dart'; export 'src/models/user.dart'; export 'src/theme/chat_theme.dart'; export 'src/theme/chat_theme_extension.dart'; diff --git a/packages/flutter_chat_core/lib/src/models/builders.dart b/packages/flutter_chat_core/lib/src/models/builders.dart index 31b3ecd3..02b05e0e 100644 --- a/packages/flutter_chat_core/lib/src/models/builders.dart +++ b/packages/flutter_chat_core/lib/src/models/builders.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../utils/typedefs.dart'; import 'message.dart'; +import 'message_group_status.dart'; part 'builders.freezed.dart'; @@ -31,8 +32,10 @@ typedef ChatMessageBuilder = Widget Function( Message message, int index, Animation animation, - Widget child, -); + Widget child, { + bool? isRemoved, + MessageGroupStatus? groupStatus, +}); typedef ChatAnimatedListBuilder = Widget Function( BuildContext, ChatItem itemBuilder, diff --git a/packages/flutter_chat_core/lib/src/models/message_group_status.dart b/packages/flutter_chat_core/lib/src/models/message_group_status.dart new file mode 100644 index 00000000..7013d9a2 --- /dev/null +++ b/packages/flutter_chat_core/lib/src/models/message_group_status.dart @@ -0,0 +1,11 @@ +class MessageGroupStatus { + final bool isFirst; + final bool isLast; + final bool isMiddle; + + const MessageGroupStatus({ + required this.isFirst, + required this.isLast, + required this.isMiddle, + }); +} diff --git a/packages/flutter_chat_core/lib/src/models/user.dart b/packages/flutter_chat_core/lib/src/models/user.dart index 67c5e331..9eaaf32a 100644 --- a/packages/flutter_chat_core/lib/src/models/user.dart +++ b/packages/flutter_chat_core/lib/src/models/user.dart @@ -11,7 +11,7 @@ class User with _$User { required String id, String? firstName, String? lastName, - String? imageUrl, + String? imageSource, @EpochDateTimeConverter() DateTime? createdAt, Map? metadata, }) = _User; diff --git a/packages/flutter_chat_core/lib/src/models/user.freezed.dart b/packages/flutter_chat_core/lib/src/models/user.freezed.dart index 9b0681bb..caf77fa9 100644 --- a/packages/flutter_chat_core/lib/src/models/user.freezed.dart +++ b/packages/flutter_chat_core/lib/src/models/user.freezed.dart @@ -23,7 +23,7 @@ mixin _$User { String get id => throw _privateConstructorUsedError; String? get firstName => throw _privateConstructorUsedError; String? get lastName => throw _privateConstructorUsedError; - String? get imageUrl => throw _privateConstructorUsedError; + String? get imageSource => throw _privateConstructorUsedError; @EpochDateTimeConverter() DateTime? get createdAt => throw _privateConstructorUsedError; Map? get metadata => throw _privateConstructorUsedError; @@ -46,7 +46,7 @@ abstract class $UserCopyWith<$Res> { {String id, String? firstName, String? lastName, - String? imageUrl, + String? imageSource, @EpochDateTimeConverter() DateTime? createdAt, Map? metadata}); } @@ -69,7 +69,7 @@ class _$UserCopyWithImpl<$Res, $Val extends User> Object? id = null, Object? firstName = freezed, Object? lastName = freezed, - Object? imageUrl = freezed, + Object? imageSource = freezed, Object? createdAt = freezed, Object? metadata = freezed, }) { @@ -86,9 +86,9 @@ class _$UserCopyWithImpl<$Res, $Val extends User> ? _value.lastName : lastName // ignore: cast_nullable_to_non_nullable as String?, - imageUrl: freezed == imageUrl - ? _value.imageUrl - : imageUrl // ignore: cast_nullable_to_non_nullable + imageSource: freezed == imageSource + ? _value.imageSource + : imageSource // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt @@ -113,7 +113,7 @@ abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { {String id, String? firstName, String? lastName, - String? imageUrl, + String? imageSource, @EpochDateTimeConverter() DateTime? createdAt, Map? metadata}); } @@ -133,7 +133,7 @@ class __$$UserImplCopyWithImpl<$Res> Object? id = null, Object? firstName = freezed, Object? lastName = freezed, - Object? imageUrl = freezed, + Object? imageSource = freezed, Object? createdAt = freezed, Object? metadata = freezed, }) { @@ -150,9 +150,9 @@ class __$$UserImplCopyWithImpl<$Res> ? _value.lastName : lastName // ignore: cast_nullable_to_non_nullable as String?, - imageUrl: freezed == imageUrl - ? _value.imageUrl - : imageUrl // ignore: cast_nullable_to_non_nullable + imageSource: freezed == imageSource + ? _value.imageSource + : imageSource // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt @@ -173,7 +173,7 @@ class _$UserImpl extends _User { {required this.id, this.firstName, this.lastName, - this.imageUrl, + this.imageSource, @EpochDateTimeConverter() this.createdAt, final Map? metadata}) : _metadata = metadata, @@ -189,7 +189,7 @@ class _$UserImpl extends _User { @override final String? lastName; @override - final String? imageUrl; + final String? imageSource; @override @EpochDateTimeConverter() final DateTime? createdAt; @@ -205,7 +205,7 @@ class _$UserImpl extends _User { @override String toString() { - return 'User(id: $id, firstName: $firstName, lastName: $lastName, imageUrl: $imageUrl, createdAt: $createdAt, metadata: $metadata)'; + return 'User(id: $id, firstName: $firstName, lastName: $lastName, imageSource: $imageSource, createdAt: $createdAt, metadata: $metadata)'; } @override @@ -218,8 +218,8 @@ class _$UserImpl extends _User { other.firstName == firstName) && (identical(other.lastName, lastName) || other.lastName == lastName) && - (identical(other.imageUrl, imageUrl) || - other.imageUrl == imageUrl) && + (identical(other.imageSource, imageSource) || + other.imageSource == imageSource) && (identical(other.createdAt, createdAt) || other.createdAt == createdAt) && const DeepCollectionEquality().equals(other._metadata, _metadata)); @@ -228,7 +228,7 @@ class _$UserImpl extends _User { @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, firstName, lastName, - imageUrl, createdAt, const DeepCollectionEquality().hash(_metadata)); + imageSource, createdAt, const DeepCollectionEquality().hash(_metadata)); /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @@ -251,7 +251,7 @@ abstract class _User extends User { {required final String id, final String? firstName, final String? lastName, - final String? imageUrl, + final String? imageSource, @EpochDateTimeConverter() final DateTime? createdAt, final Map? metadata}) = _$UserImpl; const _User._() : super._(); @@ -265,7 +265,7 @@ abstract class _User extends User { @override String? get lastName; @override - String? get imageUrl; + String? get imageSource; @override @EpochDateTimeConverter() DateTime? get createdAt; diff --git a/packages/flutter_chat_core/lib/src/models/user.g.dart b/packages/flutter_chat_core/lib/src/models/user.g.dart index c7c77cb2..c2890071 100644 --- a/packages/flutter_chat_core/lib/src/models/user.g.dart +++ b/packages/flutter_chat_core/lib/src/models/user.g.dart @@ -10,7 +10,7 @@ _$UserImpl _$$UserImplFromJson(Map json) => _$UserImpl( id: json['id'] as String, firstName: json['firstName'] as String?, lastName: json['lastName'] as String?, - imageUrl: json['imageUrl'] as String?, + imageSource: json['imageSource'] as String?, createdAt: _$JsonConverterFromJson( json['createdAt'], const EpochDateTimeConverter().fromJson), metadata: json['metadata'] as Map?, @@ -29,7 +29,7 @@ Map _$$UserImplToJson(_$UserImpl instance) { writeNotNull('firstName', instance.firstName); writeNotNull('lastName', instance.lastName); - writeNotNull('imageUrl', instance.imageUrl); + writeNotNull('imageSource', instance.imageSource); writeNotNull( 'createdAt', _$JsonConverterToJson( diff --git a/packages/flutter_chat_core/lib/src/utils/typedefs.dart b/packages/flutter_chat_core/lib/src/utils/typedefs.dart index 7e373599..4b823cd1 100644 --- a/packages/flutter_chat_core/lib/src/utils/typedefs.dart +++ b/packages/flutter_chat_core/lib/src/utils/typedefs.dart @@ -7,5 +7,6 @@ typedef ChatItem = Widget Function( Message message, int index, Animation animation, { + int? messageGroupingTimeoutInSeconds, bool? isRemoved, }); diff --git a/packages/flutter_chat_core/pubspec.yaml b/packages/flutter_chat_core/pubspec.yaml index b40b0d38..857de1c7 100644 --- a/packages/flutter_chat_core/pubspec.yaml +++ b/packages/flutter_chat_core/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_chat_core -version: 0.0.5 +version: 0.0.6 description: > Core package for Flutter chat apps, complementing flutter_chat_ui. Contains models and core functionality. #chat #ui diff --git a/packages/flutter_chat_ui/CHANGELOG.md b/packages/flutter_chat_ui/CHANGELOG.md index b6c6d193..514d3495 100644 --- a/packages/flutter_chat_ui/CHANGELOG.md +++ b/packages/flutter_chat_ui/CHANGELOG.md @@ -1,6 +1,17 @@ ## 2.0.0-dev.5 +**⚠️ Breaking changes ⚠️** + +- Changed signature of `chatMessageBuilder` to include `isRemoved` and `groupStatus` parameters. +- Changed `imageUrl` to `imageSource` for the `User` model. Change is necessary to show that not only remote URLs are supported but also local assets. +- `messageGroupingTimeoutInSeconds` is now set via `chatAnimatedListBuilder`. + +**⚠️ New features ⚠️** + - Add `hintText` to the `ChatInput` widget. +- Added avatar support. Check local example for details. +- `chatMessageBuilder` now returns `isRemoved` and `groupStatus` parameters. Group status returns information about message's position in the group. `isRemoved` is `true` if message is removed. +- Added `leadingWidget` and `trailingWidget` to the `ChatMessage` widget. ## 2.0.0-dev.4 diff --git a/packages/flutter_chat_ui/lib/flutter_chat_ui.dart b/packages/flutter_chat_ui/lib/flutter_chat_ui.dart index 6c613c66..dffa205c 100644 --- a/packages/flutter_chat_ui/lib/flutter_chat_ui.dart +++ b/packages/flutter_chat_ui/lib/flutter_chat_ui.dart @@ -1,3 +1,4 @@ +export 'src/avatar.dart'; export 'src/chat.dart'; export 'src/chat_animated_list/chat_animated_list.dart'; export 'src/chat_animated_list/chat_animated_list_reversed.dart'; diff --git a/packages/flutter_chat_ui/lib/src/avatar.dart b/packages/flutter_chat_ui/lib/src/avatar.dart new file mode 100644 index 00000000..442a87fb --- /dev/null +++ b/packages/flutter_chat_ui/lib/src/avatar.dart @@ -0,0 +1,151 @@ +import 'package:cross_cache/cross_cache.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:provider/provider.dart'; + +import 'utils/typedefs.dart'; + +class Avatar extends StatelessWidget { + final String userId; + final double? size; + final Color? backgroundColor; + final Color? foregroundColor; + final VoidCallback? onTap; + + const Avatar({ + super.key, + required this.userId, + this.size = 32, + this.backgroundColor, + this.foregroundColor, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + final userFuture = context.watch()(userId); + + return FutureBuilder( + future: userFuture, + builder: (context, snapshot) { + Widget avatar = Container( + width: size, + height: size, + decoration: BoxDecoration( + color: backgroundColor ?? theme.colors.surfaceContainer, + shape: BoxShape.circle, + ), + child: snapshot.connectionState == ConnectionState.waiting + ? null + : AvatarContent( + user: snapshot.data, + size: size, + foregroundColor: foregroundColor ?? theme.colors.onSurface, + textStyle: theme.typography.labelLarge.copyWith( + color: foregroundColor ?? theme.colors.onSurface, + fontWeight: FontWeight.bold, + ), + ), + ); + + if (onTap != null) { + avatar = GestureDetector(onTap: onTap, child: avatar); + } + + return avatar; + }, + ); + } +} + +class AvatarContent extends StatefulWidget { + final User? user; + final double? size; + final Color foregroundColor; + final TextStyle? textStyle; + + const AvatarContent({ + super.key, + required this.user, + required this.size, + required this.foregroundColor, + this.textStyle, + }); + + @override + State createState() => _AvatarContentState(); +} + +class _AvatarContentState extends State { + late CachedNetworkImage? _cachedNetworkImage; + + @override + void initState() { + super.initState(); + + final crossCache = context.read(); + + _cachedNetworkImage = widget.user?.imageSource != null + ? CachedNetworkImage(widget.user!.imageSource!, crossCache) + : null; + } + + @override + void didUpdateWidget(covariant AvatarContent oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.user?.imageSource != widget.user?.imageSource) { + final crossCache = context.read(); + final newImage = + CachedNetworkImage(widget.user!.imageSource!, crossCache); + + precacheImage(newImage, context).then((_) { + if (mounted) { + _cachedNetworkImage = newImage; + } + }); + } + } + + @override + Widget build(BuildContext context) { + if (_cachedNetworkImage != null) { + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: _cachedNetworkImage!, + fit: BoxFit.cover, + ), + ), + ); + } + + final initials = _getInitials(widget.user); + if (initials.isNotEmpty) { + return Center( + child: Text( + initials, + style: widget.textStyle, + ), + ); + } + + return Icon( + Icons.person, + color: widget.foregroundColor, + size: 24, + ); + } + + String _getInitials(User? user) { + if (user?.firstName == null && user?.lastName == null) return ''; + + final firstInitial = + user?.firstName?.isNotEmpty == true ? user!.firstName![0] : ''; + final lastInitial = + user?.lastName?.isNotEmpty == true ? user!.lastName![0] : ''; + + return '$firstInitial$lastInitial'.toUpperCase(); + } +} diff --git a/packages/flutter_chat_ui/lib/src/chat.dart b/packages/flutter_chat_ui/lib/src/chat.dart index 022b0018..c7a2e68b 100644 --- a/packages/flutter_chat_ui/lib/src/chat.dart +++ b/packages/flutter_chat_ui/lib/src/chat.dart @@ -119,6 +119,7 @@ class _ChatState extends State with WidgetsBindingObserver { Message message, int index, Animation animation, { + int? messageGroupingTimeoutInSeconds, bool? isRemoved, }) { return ChatMessageInternal( @@ -126,6 +127,7 @@ class _ChatState extends State with WidgetsBindingObserver { message: message, index: index, animation: animation, + messageGroupingTimeoutInSeconds: messageGroupingTimeoutInSeconds, isRemoved: isRemoved, ); } diff --git a/packages/flutter_chat_ui/lib/src/chat_animated_list/chat_animated_list.dart b/packages/flutter_chat_ui/lib/src/chat_animated_list/chat_animated_list.dart index 74912201..a0fcb7c3 100644 --- a/packages/flutter_chat_ui/lib/src/chat_animated_list/chat_animated_list.dart +++ b/packages/flutter_chat_ui/lib/src/chat_animated_list/chat_animated_list.dart @@ -40,6 +40,7 @@ class ChatAnimatedList extends StatefulWidget { /// 0 represents the top of the list, while 1 represents the bottom. /// A value of 0.2 means pagination will trigger when scrolled to 20% from the top. final double? paginationThreshold; + final int? messageGroupingTimeoutInSeconds; const ChatAnimatedList({ super.key, @@ -60,6 +61,7 @@ class ChatAnimatedList extends StatefulWidget { this.shouldScrollToEndWhenAtBottom = true, this.onEndReached, this.paginationThreshold = 0.2, + this.messageGroupingTimeoutInSeconds, }); @override @@ -303,6 +305,8 @@ class ChatAnimatedListState extends State message, index, animation, + messageGroupingTimeoutInSeconds: + widget.messageGroupingTimeoutInSeconds, ); }, ), @@ -618,6 +622,7 @@ class ChatAnimatedListState extends State data, position, animation, + messageGroupingTimeoutInSeconds: widget.messageGroupingTimeoutInSeconds, isRemoved: true, ), duration: widget.removeAnimationDuration, diff --git a/packages/flutter_chat_ui/lib/src/chat_animated_list/chat_animated_list_reversed.dart b/packages/flutter_chat_ui/lib/src/chat_animated_list/chat_animated_list_reversed.dart index dcdfa9b0..bd962a93 100644 --- a/packages/flutter_chat_ui/lib/src/chat_animated_list/chat_animated_list_reversed.dart +++ b/packages/flutter_chat_ui/lib/src/chat_animated_list/chat_animated_list_reversed.dart @@ -35,6 +35,7 @@ class ChatAnimatedListReversed extends StatefulWidget { /// 0 represents the top of the list, while 1 represents the bottom. /// A value of 0.2 means pagination will trigger when scrolled to 20% from the top. final double? paginationThreshold; + final int? messageGroupingTimeoutInSeconds; const ChatAnimatedListReversed({ super.key, @@ -53,6 +54,7 @@ class ChatAnimatedListReversed extends StatefulWidget { this.shouldScrollToEndWhenSendingMessage = true, this.onEndReached, this.paginationThreshold = 0.2, + this.messageGroupingTimeoutInSeconds, }); @override @@ -242,6 +244,8 @@ class ChatAnimatedListReversedState extends State message, currentIndex, animation, + messageGroupingTimeoutInSeconds: + widget.messageGroupingTimeoutInSeconds, ); }, ), @@ -436,6 +440,7 @@ class ChatAnimatedListReversedState extends State data, visualPosition, animation, + messageGroupingTimeoutInSeconds: widget.messageGroupingTimeoutInSeconds, isRemoved: true, ), duration: widget.removeAnimationDuration, diff --git a/packages/flutter_chat_ui/lib/src/chat_message/chat_message.dart b/packages/flutter_chat_ui/lib/src/chat_message/chat_message.dart index 1e199d3c..9568010e 100644 --- a/packages/flutter_chat_ui/lib/src/chat_message/chat_message.dart +++ b/packages/flutter_chat_ui/lib/src/chat_message/chat_message.dart @@ -11,6 +11,8 @@ class ChatMessage extends StatelessWidget { final int index; final Animation animation; final Widget child; + final Widget? leadingWidget; + final Widget? trailingWidget; final Alignment sentMessageScaleAnimationAlignment; final Alignment receivedMessageScaleAnimationAlignment; final AlignmentGeometry sentMessageAlignment; @@ -20,7 +22,7 @@ class ChatMessage extends StatelessWidget { final EdgeInsetsGeometry? padding; final Duration? paddingChangeAnimationDuration; final bool? isRemoved; - final int? messageGroupingTimeoutInSeconds; + final MessageGroupStatus? groupStatus; final double? horizontalPadding; final double? verticalPadding; final double? verticalGroupedPadding; @@ -31,6 +33,8 @@ class ChatMessage extends StatelessWidget { required this.index, required this.animation, required this.child, + this.leadingWidget, + this.trailingWidget, this.sentMessageScaleAnimationAlignment = Alignment.centerRight, this.receivedMessageScaleAnimationAlignment = Alignment.centerLeft, this.sentMessageAlignment = AlignmentDirectional.centerEnd, @@ -40,12 +44,22 @@ class ChatMessage extends StatelessWidget { this.padding = _sentinelPadding, this.paddingChangeAnimationDuration = const Duration(milliseconds: 250), this.isRemoved, - this.messageGroupingTimeoutInSeconds = 300, + this.groupStatus, this.horizontalPadding = 8, this.verticalPadding = 12, this.verticalGroupedPadding = 2, }); + Widget get messageRow => Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (leadingWidget != null) leadingWidget!, + Flexible(child: child), + if (trailingWidget != null) trailingWidget!, + ], + ); + @override Widget build(BuildContext context) { final onMessageTap = context.read(); @@ -82,10 +96,13 @@ class ChatMessage extends StatelessWidget { padding: resolvedPadding!, duration: paddingChangeAnimationDuration!, curve: Curves.linearToEaseOut, - child: child, + child: messageRow, + ) + : Padding( + padding: resolvedPadding!, + child: messageRow, ) - : Padding(padding: resolvedPadding!, child: child) - : child, + : messageRow, ), ), ), @@ -103,34 +120,18 @@ class ChatMessage extends StatelessWidget { return EdgeInsets.symmetric(horizontal: horizontalPadding ?? 0); } - try { - final chatController = context.read(); - final previousMessage = chatController.messages[index - 1]; - - final isGrouped = previousMessage.authorId == message.authorId && - message.createdAt.difference(previousMessage.createdAt).inSeconds < - (messageGroupingTimeoutInSeconds ?? 0); - - return isGrouped || isRemoved == true - ? EdgeInsets.fromLTRB( - horizontalPadding ?? 0, - verticalGroupedPadding ?? 0, - horizontalPadding ?? 0, - 0, - ) - : EdgeInsets.fromLTRB( - horizontalPadding ?? 0, - verticalPadding ?? 0, - horizontalPadding ?? 0, - 0, - ); - } catch (e) { - return EdgeInsets.fromLTRB( - horizontalPadding ?? 0, - verticalPadding ?? 0, - horizontalPadding ?? 0, - 0, - ); - } + return groupStatus?.isFirst == false || isRemoved == true + ? EdgeInsets.fromLTRB( + horizontalPadding ?? 0, + verticalGroupedPadding ?? 0, + horizontalPadding ?? 0, + 0, + ) + : EdgeInsets.fromLTRB( + horizontalPadding ?? 0, + verticalPadding ?? 0, + horizontalPadding ?? 0, + 0, + ); } } diff --git a/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart b/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart index 3880b640..5d9d585e 100644 --- a/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart +++ b/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart @@ -11,6 +11,7 @@ class ChatMessageInternal extends StatefulWidget { final Message message; final int index; final Animation animation; + final int? messageGroupingTimeoutInSeconds; final bool? isRemoved; const ChatMessageInternal({ @@ -18,6 +19,7 @@ class ChatMessageInternal extends StatefulWidget { required this.message, required this.index, required this.animation, + this.messageGroupingTimeoutInSeconds, this.isRemoved, }); @@ -78,22 +80,65 @@ class ChatMessageInternalState extends State { widget.index, ); + final groupStatus = _resolveGroupStatus(context); + return builders.chatMessageBuilder?.call( context, _updatedMessage, widget.index, widget.animation, child, + isRemoved: widget.isRemoved, + groupStatus: groupStatus, ) ?? ChatMessage( message: _updatedMessage, index: widget.index, animation: widget.animation, isRemoved: widget.isRemoved, + groupStatus: groupStatus, child: child, ); } + MessageGroupStatus? _resolveGroupStatus(BuildContext context) { + final chatController = context.read(); + final messages = chatController.messages; + final index = widget.index; + final currentMessage = _updatedMessage; + final timeoutInSeconds = widget.messageGroupingTimeoutInSeconds ?? 300; + + // Get adjacent messages if they exist + final nextMessage = + index < messages.length - 1 ? messages[index + 1] : null; + final previousMessage = index > 0 ? messages[index - 1] : null; + + // Check if message is part of a group with next message + final isGroupedWithNext = nextMessage != null && + nextMessage.authorId == currentMessage.authorId && + nextMessage.createdAt.difference(currentMessage.createdAt).inSeconds < + timeoutInSeconds; + + // Check if message is part of a group with previous message + final isGroupedWithPrevious = previousMessage != null && + previousMessage.authorId == currentMessage.authorId && + currentMessage.createdAt + .difference(previousMessage.createdAt) + .inSeconds < + timeoutInSeconds; + + // If not grouped with either message, return null + if (!isGroupedWithNext && !isGroupedWithPrevious) { + return null; + } + + return MessageGroupStatus( + isFirst: !isGroupedWithPrevious, + isLast: !isGroupedWithNext, + isMiddle: isGroupedWithNext && isGroupedWithPrevious, + ); + } + Widget _buildMessage( BuildContext context, Builders builders, diff --git a/packages/flutter_chat_ui/pubspec.yaml b/packages/flutter_chat_ui/pubspec.yaml index 613c888a..2e042bb2 100644 --- a/packages/flutter_chat_ui/pubspec.yaml +++ b/packages/flutter_chat_ui/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_chat_ui -version: 2.0.0-dev.4 +version: 2.0.0-dev.5 description: > Community-driven, animated chat UI for Flutter with top-tier performance and customization for chat apps and generative AI agents. #firebase #gemini #chatgpt @@ -11,11 +11,11 @@ environment: flutter: ">=3.19.0" dependencies: - cross_cache: ^0.0.5 + cross_cache: ^0.0.6 diffutil_dart: ^4.0.1 flutter: sdk: flutter - flutter_chat_core: ^0.0.5 + flutter_chat_core: ^0.0.6 provider: ^6.1.2 scrollview_observer: ^1.24.0 diff --git a/packages/flyer_chat_audio_message/CHANGELOG.md b/packages/flyer_chat_audio_message/CHANGELOG.md index 1ed209b7..b24d10bd 100644 --- a/packages/flyer_chat_audio_message/CHANGELOG.md +++ b/packages/flyer_chat_audio_message/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Version bump to match other packages diff --git a/packages/flyer_chat_audio_message/pubspec.yaml b/packages/flyer_chat_audio_message/pubspec.yaml index 6ae7e34b..7e78d353 100644 --- a/packages/flyer_chat_audio_message/pubspec.yaml +++ b/packages/flyer_chat_audio_message/pubspec.yaml @@ -1,5 +1,5 @@ name: flyer_chat_audio_message -version: 0.0.5 +version: 0.0.6 description: > Audio message package for Flutter chat apps, complementing flutter_chat_ui. #chat #ui homepage: https://flyer.chat diff --git a/packages/flyer_chat_custom_message/CHANGELOG.md b/packages/flyer_chat_custom_message/CHANGELOG.md index 1ed209b7..b24d10bd 100644 --- a/packages/flyer_chat_custom_message/CHANGELOG.md +++ b/packages/flyer_chat_custom_message/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Version bump to match other packages diff --git a/packages/flyer_chat_custom_message/pubspec.yaml b/packages/flyer_chat_custom_message/pubspec.yaml index 5006bd64..ad1a2682 100644 --- a/packages/flyer_chat_custom_message/pubspec.yaml +++ b/packages/flyer_chat_custom_message/pubspec.yaml @@ -1,5 +1,5 @@ name: flyer_chat_custom_message -version: 0.0.5 +version: 0.0.6 description: > Custom message package for Flutter chat apps, complementing flutter_chat_ui. #chat #ui homepage: https://flyer.chat diff --git a/packages/flyer_chat_file_message/CHANGELOG.md b/packages/flyer_chat_file_message/CHANGELOG.md index 1ed209b7..b24d10bd 100644 --- a/packages/flyer_chat_file_message/CHANGELOG.md +++ b/packages/flyer_chat_file_message/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Version bump to match other packages diff --git a/packages/flyer_chat_file_message/pubspec.yaml b/packages/flyer_chat_file_message/pubspec.yaml index 44c3d2df..7f07467f 100644 --- a/packages/flyer_chat_file_message/pubspec.yaml +++ b/packages/flyer_chat_file_message/pubspec.yaml @@ -1,5 +1,5 @@ name: flyer_chat_file_message -version: 0.0.5 +version: 0.0.6 description: > File message package for Flutter chat apps, complementing flutter_chat_ui. #chat #ui homepage: https://flyer.chat diff --git a/packages/flyer_chat_image_message/CHANGELOG.md b/packages/flyer_chat_image_message/CHANGELOG.md index e1dbbea5..a1b81475 100644 --- a/packages/flyer_chat_image_message/CHANGELOG.md +++ b/packages/flyer_chat_image_message/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Introduce new customization options 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 79fa97b6..baeaa38f 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 @@ -103,6 +103,7 @@ class FlyerChatImageMessageState extends State @override void didUpdateWidget(covariant FlyerChatImageMessage oldWidget) { + super.didUpdateWidget(oldWidget); if (oldWidget.message.source != widget.message.source) { final crossCache = context.read(); final newImage = CachedNetworkImage(widget.message.source, crossCache); @@ -113,7 +114,6 @@ class FlyerChatImageMessageState extends State } }); } - super.didUpdateWidget(oldWidget); } @override diff --git a/packages/flyer_chat_image_message/pubspec.yaml b/packages/flyer_chat_image_message/pubspec.yaml index ee821b1b..1d74df08 100644 --- a/packages/flyer_chat_image_message/pubspec.yaml +++ b/packages/flyer_chat_image_message/pubspec.yaml @@ -1,5 +1,5 @@ name: flyer_chat_image_message -version: 0.0.5 +version: 0.0.6 description: > Image message package for Flutter chat apps, complementing flutter_chat_ui. Supports caching, ThumbHash and BlurHash. #chat #ui @@ -12,10 +12,10 @@ environment: dependencies: blurhash_dart: ^1.2.1 - cross_cache: ^0.0.5 + cross_cache: ^0.0.6 flutter: sdk: flutter - flutter_chat_core: ^0.0.5 + flutter_chat_core: ^0.0.6 image: ^4.5.2 provider: ^6.1.2 thumbhash: ^0.1.0+1 diff --git a/packages/flyer_chat_location_message/CHANGELOG.md b/packages/flyer_chat_location_message/CHANGELOG.md index 1ed209b7..b24d10bd 100644 --- a/packages/flyer_chat_location_message/CHANGELOG.md +++ b/packages/flyer_chat_location_message/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Version bump to match other packages diff --git a/packages/flyer_chat_location_message/pubspec.yaml b/packages/flyer_chat_location_message/pubspec.yaml index ba28538d..142742b1 100644 --- a/packages/flyer_chat_location_message/pubspec.yaml +++ b/packages/flyer_chat_location_message/pubspec.yaml @@ -1,5 +1,5 @@ name: flyer_chat_location_message -version: 0.0.5 +version: 0.0.6 description: > Location message package for Flutter chat apps, complementing flutter_chat_ui. #chat #ui homepage: https://flyer.chat diff --git a/packages/flyer_chat_system_message/CHANGELOG.md b/packages/flyer_chat_system_message/CHANGELOG.md index 1ed209b7..b24d10bd 100644 --- a/packages/flyer_chat_system_message/CHANGELOG.md +++ b/packages/flyer_chat_system_message/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Version bump to match other packages diff --git a/packages/flyer_chat_system_message/pubspec.yaml b/packages/flyer_chat_system_message/pubspec.yaml index 49e19bd9..ce4f2c81 100644 --- a/packages/flyer_chat_system_message/pubspec.yaml +++ b/packages/flyer_chat_system_message/pubspec.yaml @@ -1,5 +1,5 @@ name: flyer_chat_system_message -version: 0.0.5 +version: 0.0.6 description: > System message package for Flutter chat apps, complementing flutter_chat_ui. #chat #ui homepage: https://flyer.chat diff --git a/packages/flyer_chat_text_message/CHANGELOG.md b/packages/flyer_chat_text_message/CHANGELOG.md index 82273ef0..84937c87 100644 --- a/packages/flyer_chat_text_message/CHANGELOG.md +++ b/packages/flyer_chat_text_message/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Introduce new customization options diff --git a/packages/flyer_chat_text_message/pubspec.yaml b/packages/flyer_chat_text_message/pubspec.yaml index fb1a1fa8..bca65911 100644 --- a/packages/flyer_chat_text_message/pubspec.yaml +++ b/packages/flyer_chat_text_message/pubspec.yaml @@ -1,5 +1,5 @@ name: flyer_chat_text_message -version: 0.0.5 +version: 0.0.6 description: > Text message package for Flutter chat apps, complementing flutter_chat_ui. #chat #ui homepage: https://flyer.chat @@ -12,7 +12,7 @@ environment: dependencies: flutter: sdk: flutter - flutter_chat_core: ^0.0.5 + flutter_chat_core: ^0.0.6 flutter_markdown: ^0.7.4+3 provider: ^6.1.2 diff --git a/packages/flyer_chat_video_message/CHANGELOG.md b/packages/flyer_chat_video_message/CHANGELOG.md index 1ed209b7..b24d10bd 100644 --- a/packages/flyer_chat_video_message/CHANGELOG.md +++ b/packages/flyer_chat_video_message/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.6 + +- Version bump to match other packages + ## 0.0.5 - Version bump to match other packages diff --git a/packages/flyer_chat_video_message/pubspec.yaml b/packages/flyer_chat_video_message/pubspec.yaml index fe129192..bfd28adf 100644 --- a/packages/flyer_chat_video_message/pubspec.yaml +++ b/packages/flyer_chat_video_message/pubspec.yaml @@ -1,5 +1,5 @@ name: flyer_chat_video_message -version: 0.0.5 +version: 0.0.6 description: > Video message package for Flutter chat apps, complementing flutter_chat_ui. #chat #ui homepage: https://flyer.chat