diff --git a/examples/flyer_chat/ios/Podfile.lock b/examples/flyer_chat/ios/Podfile.lock index 81472501a..35a65cfd2 100644 --- a/examples/flyer_chat/ios/Podfile.lock +++ b/examples/flyer_chat/ios/Podfile.lock @@ -30,6 +30,8 @@ PODS: - DKPhotoGallery/Resource (0.0.19): - SDWebImage - SwiftyGif + - emoji_picker_flutter (0.0.1): + - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter @@ -43,20 +45,25 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - SDWebImage (5.21.0): - - SDWebImage/Core (= 5.21.0) - - SDWebImage/Core (5.21.0) + - SDWebImage (5.21.1): + - SDWebImage/Core (= 5.21.1) + - SDWebImage/Core (5.21.1) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter DEPENDENCIES: + - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -67,6 +74,8 @@ SPEC REPOS: - SwiftyGif EXTERNAL SOURCES: + emoji_picker_flutter: + :path: ".symlinks/plugins/emoji_picker_flutter/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: @@ -79,19 +88,23 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/isar_flutter_libs/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e isar_flutter_libs: 9fc2cfb928c539e1b76c481ba5d143d556d94920 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 + SDWebImage: f29024626962457f3470184232766516dee8dfea + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d diff --git a/examples/flyer_chat/lib/api/api.dart b/examples/flyer_chat/lib/api/api.dart index d6175bbcb..2e2b271e0 100644 --- a/examples/flyer_chat/lib/api/api.dart +++ b/examples/flyer_chat/lib/api/api.dart @@ -294,6 +294,7 @@ class ApiState extends State { Message item, { int? index, TapUpDetails? details, + required bool isSentByMe, }) async { await _chatController.removeMessage(item); diff --git a/examples/flyer_chat/lib/create_message.dart b/examples/flyer_chat/lib/create_message.dart index f56ee2f36..07369b570 100644 --- a/examples/flyer_chat/lib/create_message.dart +++ b/examples/flyer_chat/lib/create_message.dart @@ -25,6 +25,13 @@ Future createMessage( sentAt: localOnly == true ? DateTime.now().toUtc() : null, text: text ?? lorem(paragraphs: 1, words: Random().nextInt(30) + 1), metadata: isOnlyEmoji(text ?? '') ? {'isOnlyEmoji': true} : null, + reactions: { + '👍': [authorId, 'someOtherId'], + '👎': ['someOtherId'], + '👏': [authorId], + '👌': [authorId], + '👊': [authorId], + }, ); } else { final orientation = ['portrait', 'square', 'wide'][Random().nextInt(3)]; @@ -61,6 +68,13 @@ Future createMessage( source: response.data['img'], thumbhash: response.data['thumbhash'], blurhash: response.data['blurhash'], + reactions: { + '👍': [authorId, 'someOtherId'], + '👎': ['someOtherId'], + '👏': [authorId], + '👌': [authorId], + '👊': [authorId], + }, ); } else { message = FileMessage( @@ -71,6 +85,13 @@ Future createMessage( sentAt: localOnly == true ? DateTime.now().toUtc() : null, source: response.data['img'], size: 1000000, + reactions: { + '👍': [authorId, 'someOtherId'], + '👎': ['someOtherId'], + '👏': [authorId], + '👌': [authorId], + '👊': [authorId], + }, ); } } diff --git a/examples/flyer_chat/lib/local.dart b/examples/flyer_chat/lib/local.dart index 11a893f9d..1abc726ee 100644 --- a/examples/flyer_chat/lib/local.dart +++ b/examples/flyer_chat/lib/local.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:dio/dio.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -10,6 +11,7 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_link_previewer/flutter_link_previewer.dart'; import 'package:flyer_chat_file_message/flyer_chat_file_message.dart'; import 'package:flyer_chat_image_message/flyer_chat_image_message.dart'; +import 'package:flyer_chat_reactions/flyer_chat_reactions.dart'; import 'package:flyer_chat_system_message/flyer_chat_system_message.dart'; import 'package:flyer_chat_text_message/flyer_chat_text_message.dart'; import 'package:image_picker/image_picker.dart'; @@ -233,6 +235,30 @@ class LocalState extends State { child: child, ); }, + reactionsBuilder: (context, message, isSentByMe) { + final reactions = reactionsFromMessageReactions( + reactions: message.reactions, + currentUserId: _currentUser.id, + ); + return FlyerChatReactionsRow( + reactions: reactions, + alignment: isSentByMe + ? MainAxisAlignment.start + : MainAxisAlignment.end, + + /// Open List on tap (WhatsApp Style) + // onReactionTap: (reaction) => + // showReactionsList(context: context, reactions: reactions), + /// Or react on tap (Slack Style) + onReactionTap: (reaction) => + _handleReactionTap(message, reaction), + removeOrAddLocallyOnTap: true, + onReactionLongPress: (reaction) => + showReactionsList(context: context, reactions: reactions), + onSurplusReactionTap: () => + showReactionsList(context: context, reactions: reactions), + ); + }, ), chatController: _chatController, currentUserId: _currentUser.id, @@ -272,19 +298,75 @@ class LocalState extends State { Message message, { int? index, LongPressStartDetails? details, + required bool isSentByMe, }) async { - // Skip showing menu for system messages - if (message.authorId == 'system' || details == null) return; + if (message.authorId == 'system') return; + showReactionsDialog( + context, + message, + isSentByMe: isSentByMe, + // reactions: ['📌'], // The default reactions to propose in the dialog + userReactions: getUserReactions(message.reactions, _currentUser.id), + onReactionTap: (reaction) => _handleReactionTap(message, reaction), + onMoreReactionsTap: () async { + // Use whichever emoji picker you want + final picked = await _showEmojiPicker(); + if (picked != null) { + _handleReactionTap(message, picked); + } + }, + bottomWidgetBuilder: (context) => _buildContextualMenu(message), + ); + } + + Future _showEmojiPicker() { + return showModalBottomSheet( + context: context, + useSafeArea: true, + builder: (context) => EmojiPicker( + onEmojiSelected: (Category? category, Emoji emoji) { + Navigator.of(context).pop(emoji.emoji); + }, + config: Config( + height: 250, + checkPlatformCompatibility: false, + viewOrderConfig: const ViewOrderConfig(), + skinToneConfig: const SkinToneConfig(), + categoryViewConfig: const CategoryViewConfig(), + bottomActionBarConfig: const BottomActionBarConfig(enabled: false), + searchViewConfig: const SearchViewConfig(), + ), + ), + ); + } - // Calculate position for the menu - final position = details.globalPosition; + void _handleReactionTap(Message message, String reaction) { + debugPrint('reaction Tapped: $reaction'); + // Maybe the lib could expose if it's a removal or at least helpers methods + final reactions = Map>.from(message.reactions ?? {}); + final userId = _currentUser.id; - // Create a Rect for the menu position (small area around tap point) - final menuRect = Rect.fromCenter( - center: position, - width: 0, // Width and height of 0 means show exactly at the point - height: 0, + final users = List.from(reactions[reaction] ?? []); + if (users.contains(userId)) { + users.remove(userId); + if (users.isEmpty) { + reactions.remove(reaction); // Remove the key if no users left + } else { + reactions[reaction] = users; + } + } else { + users.add(userId); + reactions[reaction] = users; + } + + _chatController.updateMessage( + message, + message.copyWith(reactions: reactions), ); + } + + Widget _buildContextualMenu(Message message) { + if (message.authorId == 'system') return SizedBox.shrink(); final items = [ if (message is TextMessage) @@ -293,6 +375,7 @@ class LocalState extends State { icon: CupertinoIcons.doc_on_doc, onTap: () { _copyMessage(message); + Navigator.of(context).pop(); }, ), PullDownMenuItem( @@ -301,11 +384,11 @@ class LocalState extends State { isDestructive: true, onTap: () { _removeItem(message); + Navigator.of(context).pop(); }, ), ]; - - await showPullDownMenu(context: context, position: menuRect, items: items); + return PullDownMenu(items: items); } void _copyMessage(TextMessage message) async { diff --git a/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc b/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc index 31124ea32..9ee1a82e9 100644 --- a/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc +++ b/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin"); + emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/examples/flyer_chat/linux/flutter/generated_plugins.cmake b/examples/flyer_chat/linux/flutter/generated_plugins.cmake index 00d762d49..fcacb86fe 100644 --- a/examples/flyer_chat/linux/flutter/generated_plugins.cmake +++ b/examples/flyer_chat/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter file_selector_linux isar_flutter_libs url_launcher_linux diff --git a/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift index d48ecf7cf..d3828c3e9 100644 --- a/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,16 +5,20 @@ import FlutterMacOS import Foundation +import emoji_picker_flutter import file_picker import file_selector_macos import isar_flutter_libs import path_provider_foundation +import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/examples/flyer_chat/macos/Podfile.lock b/examples/flyer_chat/macos/Podfile.lock index 9da77816f..7f4faab38 100644 --- a/examples/flyer_chat/macos/Podfile.lock +++ b/examples/flyer_chat/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - emoji_picker_flutter (0.0.1): + - FlutterMacOS - file_picker (0.0.1): - FlutterMacOS - file_selector_macos (0.0.1): @@ -9,18 +11,25 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS DEPENDENCIES: + - emoji_picker_flutter (from `Flutter/ephemeral/.symlinks/plugins/emoji_picker_flutter/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: + emoji_picker_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/emoji_picker_flutter/macos file_picker: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos file_selector_macos: @@ -31,15 +40,19 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + emoji_picker_flutter: 51ca408e289d84d1e460016b2a28721ec754fcf7 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 isar_flutter_libs: a65381780401f81ad6bf3f2e7cd0de5698fb98c4 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 diff --git a/examples/flyer_chat/pubspec.yaml b/examples/flyer_chat/pubspec.yaml index 0ac6e3ca9..3b27f4a65 100644 --- a/examples/flyer_chat/pubspec.yaml +++ b/examples/flyer_chat/pubspec.yaml @@ -2,7 +2,7 @@ name: flyer_chat description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -33,6 +33,7 @@ dependencies: cross_cache: ^1.0.4 cupertino_icons: ^1.0.8 dio: ^5.8.0+1 + emoji_picker_flutter: ^4.3.0 file_picker: ^10.2.0 flutter: sdk: flutter @@ -43,6 +44,7 @@ dependencies: flutter_lorem: ^2.0.0 flyer_chat_file_message: ^2.3.1 flyer_chat_image_message: ^2.2.1 + flyer_chat_reactions: ^0.0.12 flyer_chat_system_message: ^2.1.13 flyer_chat_text_message: ^2.5.1 flyer_chat_text_stream_message: ^2.2.6 @@ -77,7 +79,6 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc b/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc index f380d6e46..5df4cee79 100644 --- a/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc +++ b/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + EmojiPickerFlutterPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); IsarFlutterLibsPluginRegisterWithRegistrar( diff --git a/examples/flyer_chat/windows/flutter/generated_plugins.cmake b/examples/flyer_chat/windows/flutter/generated_plugins.cmake index 383a7fda4..0f17407c9 100644 --- a/examples/flyer_chat/windows/flutter/generated_plugins.cmake +++ b/examples/flyer_chat/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter file_selector_windows isar_flutter_libs url_launcher_windows diff --git a/packages/flutter_chat_core/lib/src/models/builders.dart b/packages/flutter_chat_core/lib/src/models/builders.dart index a490a028d..91f5eef20 100644 --- a/packages/flutter_chat_core/lib/src/models/builders.dart +++ b/packages/flutter_chat_core/lib/src/models/builders.dart @@ -134,6 +134,10 @@ typedef EmptyChatListBuilder = Widget Function(BuildContext); typedef LinkPreviewBuilder = Widget? Function(BuildContext, TextMessage, bool isSendByMe); +/// Signature for building the reactions widget. +typedef ReactionsBuilder = + Widget? Function(BuildContext, Message, bool isSentByMe); + /// A collection of builder functions used to customize the UI components /// of the chat interface. @Freezed(fromJson: false, toJson: false) @@ -190,6 +194,9 @@ abstract class Builders with _$Builders { /// Custom builder for the link preview widget. LinkPreviewBuilder? linkPreviewBuilder, + + /// Custom builder for the reactions widget. + ReactionsBuilder? reactionsBuilder, }) = _Builders; const Builders._(); diff --git a/packages/flutter_chat_core/lib/src/models/builders.freezed.dart b/packages/flutter_chat_core/lib/src/models/builders.freezed.dart index 78c1c7c36..1890af0ae 100644 --- a/packages/flutter_chat_core/lib/src/models/builders.freezed.dart +++ b/packages/flutter_chat_core/lib/src/models/builders.freezed.dart @@ -31,7 +31,8 @@ mixin _$Builders { ScrollToBottomBuilder? get scrollToBottomBuilder;/// Custom builder for the load more indicator. LoadMoreBuilder? get loadMoreBuilder;/// Custom builder for the empty chat list. EmptyChatListBuilder? get emptyChatListBuilder;/// Custom builder for the link preview widget. - LinkPreviewBuilder? get linkPreviewBuilder; + LinkPreviewBuilder? get linkPreviewBuilder;/// Custom builder for the reactions widget. + ReactionsBuilder? get reactionsBuilder; /// Create a copy of Builders /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -42,16 +43,16 @@ $BuildersCopyWith get copyWith => _$BuildersCopyWithImpl(thi @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is Builders&&(identical(other.textMessageBuilder, textMessageBuilder) || other.textMessageBuilder == textMessageBuilder)&&(identical(other.textStreamMessageBuilder, textStreamMessageBuilder) || other.textStreamMessageBuilder == textStreamMessageBuilder)&&(identical(other.imageMessageBuilder, imageMessageBuilder) || other.imageMessageBuilder == imageMessageBuilder)&&(identical(other.fileMessageBuilder, fileMessageBuilder) || other.fileMessageBuilder == fileMessageBuilder)&&(identical(other.videoMessageBuilder, videoMessageBuilder) || other.videoMessageBuilder == videoMessageBuilder)&&(identical(other.audioMessageBuilder, audioMessageBuilder) || other.audioMessageBuilder == audioMessageBuilder)&&(identical(other.systemMessageBuilder, systemMessageBuilder) || other.systemMessageBuilder == systemMessageBuilder)&&(identical(other.customMessageBuilder, customMessageBuilder) || other.customMessageBuilder == customMessageBuilder)&&(identical(other.unsupportedMessageBuilder, unsupportedMessageBuilder) || other.unsupportedMessageBuilder == unsupportedMessageBuilder)&&(identical(other.composerBuilder, composerBuilder) || other.composerBuilder == composerBuilder)&&(identical(other.chatMessageBuilder, chatMessageBuilder) || other.chatMessageBuilder == chatMessageBuilder)&&(identical(other.chatAnimatedListBuilder, chatAnimatedListBuilder) || other.chatAnimatedListBuilder == chatAnimatedListBuilder)&&(identical(other.scrollToBottomBuilder, scrollToBottomBuilder) || other.scrollToBottomBuilder == scrollToBottomBuilder)&&(identical(other.loadMoreBuilder, loadMoreBuilder) || other.loadMoreBuilder == loadMoreBuilder)&&(identical(other.emptyChatListBuilder, emptyChatListBuilder) || other.emptyChatListBuilder == emptyChatListBuilder)&&(identical(other.linkPreviewBuilder, linkPreviewBuilder) || other.linkPreviewBuilder == linkPreviewBuilder)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is Builders&&(identical(other.textMessageBuilder, textMessageBuilder) || other.textMessageBuilder == textMessageBuilder)&&(identical(other.textStreamMessageBuilder, textStreamMessageBuilder) || other.textStreamMessageBuilder == textStreamMessageBuilder)&&(identical(other.imageMessageBuilder, imageMessageBuilder) || other.imageMessageBuilder == imageMessageBuilder)&&(identical(other.fileMessageBuilder, fileMessageBuilder) || other.fileMessageBuilder == fileMessageBuilder)&&(identical(other.videoMessageBuilder, videoMessageBuilder) || other.videoMessageBuilder == videoMessageBuilder)&&(identical(other.audioMessageBuilder, audioMessageBuilder) || other.audioMessageBuilder == audioMessageBuilder)&&(identical(other.systemMessageBuilder, systemMessageBuilder) || other.systemMessageBuilder == systemMessageBuilder)&&(identical(other.customMessageBuilder, customMessageBuilder) || other.customMessageBuilder == customMessageBuilder)&&(identical(other.unsupportedMessageBuilder, unsupportedMessageBuilder) || other.unsupportedMessageBuilder == unsupportedMessageBuilder)&&(identical(other.composerBuilder, composerBuilder) || other.composerBuilder == composerBuilder)&&(identical(other.chatMessageBuilder, chatMessageBuilder) || other.chatMessageBuilder == chatMessageBuilder)&&(identical(other.chatAnimatedListBuilder, chatAnimatedListBuilder) || other.chatAnimatedListBuilder == chatAnimatedListBuilder)&&(identical(other.scrollToBottomBuilder, scrollToBottomBuilder) || other.scrollToBottomBuilder == scrollToBottomBuilder)&&(identical(other.loadMoreBuilder, loadMoreBuilder) || other.loadMoreBuilder == loadMoreBuilder)&&(identical(other.emptyChatListBuilder, emptyChatListBuilder) || other.emptyChatListBuilder == emptyChatListBuilder)&&(identical(other.linkPreviewBuilder, linkPreviewBuilder) || other.linkPreviewBuilder == linkPreviewBuilder)&&(identical(other.reactionsBuilder, reactionsBuilder) || other.reactionsBuilder == reactionsBuilder)); } @override -int get hashCode => Object.hash(runtimeType,textMessageBuilder,textStreamMessageBuilder,imageMessageBuilder,fileMessageBuilder,videoMessageBuilder,audioMessageBuilder,systemMessageBuilder,customMessageBuilder,unsupportedMessageBuilder,composerBuilder,chatMessageBuilder,chatAnimatedListBuilder,scrollToBottomBuilder,loadMoreBuilder,emptyChatListBuilder,linkPreviewBuilder); +int get hashCode => Object.hash(runtimeType,textMessageBuilder,textStreamMessageBuilder,imageMessageBuilder,fileMessageBuilder,videoMessageBuilder,audioMessageBuilder,systemMessageBuilder,customMessageBuilder,unsupportedMessageBuilder,composerBuilder,chatMessageBuilder,chatAnimatedListBuilder,scrollToBottomBuilder,loadMoreBuilder,emptyChatListBuilder,linkPreviewBuilder,reactionsBuilder); @override String toString() { - return 'Builders(textMessageBuilder: $textMessageBuilder, textStreamMessageBuilder: $textStreamMessageBuilder, imageMessageBuilder: $imageMessageBuilder, fileMessageBuilder: $fileMessageBuilder, videoMessageBuilder: $videoMessageBuilder, audioMessageBuilder: $audioMessageBuilder, systemMessageBuilder: $systemMessageBuilder, customMessageBuilder: $customMessageBuilder, unsupportedMessageBuilder: $unsupportedMessageBuilder, composerBuilder: $composerBuilder, chatMessageBuilder: $chatMessageBuilder, chatAnimatedListBuilder: $chatAnimatedListBuilder, scrollToBottomBuilder: $scrollToBottomBuilder, loadMoreBuilder: $loadMoreBuilder, emptyChatListBuilder: $emptyChatListBuilder, linkPreviewBuilder: $linkPreviewBuilder)'; + return 'Builders(textMessageBuilder: $textMessageBuilder, textStreamMessageBuilder: $textStreamMessageBuilder, imageMessageBuilder: $imageMessageBuilder, fileMessageBuilder: $fileMessageBuilder, videoMessageBuilder: $videoMessageBuilder, audioMessageBuilder: $audioMessageBuilder, systemMessageBuilder: $systemMessageBuilder, customMessageBuilder: $customMessageBuilder, unsupportedMessageBuilder: $unsupportedMessageBuilder, composerBuilder: $composerBuilder, chatMessageBuilder: $chatMessageBuilder, chatAnimatedListBuilder: $chatAnimatedListBuilder, scrollToBottomBuilder: $scrollToBottomBuilder, loadMoreBuilder: $loadMoreBuilder, emptyChatListBuilder: $emptyChatListBuilder, linkPreviewBuilder: $linkPreviewBuilder, reactionsBuilder: $reactionsBuilder)'; } @@ -62,7 +63,7 @@ abstract mixin class $BuildersCopyWith<$Res> { factory $BuildersCopyWith(Builders value, $Res Function(Builders) _then) = _$BuildersCopyWithImpl; @useResult $Res call({ - TextMessageBuilder? textMessageBuilder, TextStreamMessageBuilder? textStreamMessageBuilder, ImageMessageBuilder? imageMessageBuilder, FileMessageBuilder? fileMessageBuilder, VideoMessageBuilder? videoMessageBuilder, AudioMessageBuilder? audioMessageBuilder, SystemMessageBuilder? systemMessageBuilder, CustomMessageBuilder? customMessageBuilder, UnsupportedMessageBuilder? unsupportedMessageBuilder, ComposerBuilder? composerBuilder, ChatMessageBuilder? chatMessageBuilder, ChatAnimatedListBuilder? chatAnimatedListBuilder, ScrollToBottomBuilder? scrollToBottomBuilder, LoadMoreBuilder? loadMoreBuilder, EmptyChatListBuilder? emptyChatListBuilder, LinkPreviewBuilder? linkPreviewBuilder + TextMessageBuilder? textMessageBuilder, TextStreamMessageBuilder? textStreamMessageBuilder, ImageMessageBuilder? imageMessageBuilder, FileMessageBuilder? fileMessageBuilder, VideoMessageBuilder? videoMessageBuilder, AudioMessageBuilder? audioMessageBuilder, SystemMessageBuilder? systemMessageBuilder, CustomMessageBuilder? customMessageBuilder, UnsupportedMessageBuilder? unsupportedMessageBuilder, ComposerBuilder? composerBuilder, ChatMessageBuilder? chatMessageBuilder, ChatAnimatedListBuilder? chatAnimatedListBuilder, ScrollToBottomBuilder? scrollToBottomBuilder, LoadMoreBuilder? loadMoreBuilder, EmptyChatListBuilder? emptyChatListBuilder, LinkPreviewBuilder? linkPreviewBuilder, ReactionsBuilder? reactionsBuilder }); @@ -79,7 +80,7 @@ class _$BuildersCopyWithImpl<$Res> /// Create a copy of Builders /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? textMessageBuilder = freezed,Object? textStreamMessageBuilder = freezed,Object? imageMessageBuilder = freezed,Object? fileMessageBuilder = freezed,Object? videoMessageBuilder = freezed,Object? audioMessageBuilder = freezed,Object? systemMessageBuilder = freezed,Object? customMessageBuilder = freezed,Object? unsupportedMessageBuilder = freezed,Object? composerBuilder = freezed,Object? chatMessageBuilder = freezed,Object? chatAnimatedListBuilder = freezed,Object? scrollToBottomBuilder = freezed,Object? loadMoreBuilder = freezed,Object? emptyChatListBuilder = freezed,Object? linkPreviewBuilder = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? textMessageBuilder = freezed,Object? textStreamMessageBuilder = freezed,Object? imageMessageBuilder = freezed,Object? fileMessageBuilder = freezed,Object? videoMessageBuilder = freezed,Object? audioMessageBuilder = freezed,Object? systemMessageBuilder = freezed,Object? customMessageBuilder = freezed,Object? unsupportedMessageBuilder = freezed,Object? composerBuilder = freezed,Object? chatMessageBuilder = freezed,Object? chatAnimatedListBuilder = freezed,Object? scrollToBottomBuilder = freezed,Object? loadMoreBuilder = freezed,Object? emptyChatListBuilder = freezed,Object? linkPreviewBuilder = freezed,Object? reactionsBuilder = freezed,}) { return _then(_self.copyWith( textMessageBuilder: freezed == textMessageBuilder ? _self.textMessageBuilder : textMessageBuilder // ignore: cast_nullable_to_non_nullable as TextMessageBuilder?,textStreamMessageBuilder: freezed == textStreamMessageBuilder ? _self.textStreamMessageBuilder : textStreamMessageBuilder // ignore: cast_nullable_to_non_nullable @@ -97,7 +98,8 @@ as ChatAnimatedListBuilder?,scrollToBottomBuilder: freezed == scrollToBottomBuil as ScrollToBottomBuilder?,loadMoreBuilder: freezed == loadMoreBuilder ? _self.loadMoreBuilder : loadMoreBuilder // ignore: cast_nullable_to_non_nullable as LoadMoreBuilder?,emptyChatListBuilder: freezed == emptyChatListBuilder ? _self.emptyChatListBuilder : emptyChatListBuilder // ignore: cast_nullable_to_non_nullable as EmptyChatListBuilder?,linkPreviewBuilder: freezed == linkPreviewBuilder ? _self.linkPreviewBuilder : linkPreviewBuilder // ignore: cast_nullable_to_non_nullable -as LinkPreviewBuilder?, +as LinkPreviewBuilder?,reactionsBuilder: freezed == reactionsBuilder ? _self.reactionsBuilder : reactionsBuilder // ignore: cast_nullable_to_non_nullable +as ReactionsBuilder?, )); } @@ -108,7 +110,7 @@ as LinkPreviewBuilder?, class _Builders extends Builders { - const _Builders({this.textMessageBuilder, this.textStreamMessageBuilder, this.imageMessageBuilder, this.fileMessageBuilder, this.videoMessageBuilder, this.audioMessageBuilder, this.systemMessageBuilder, this.customMessageBuilder, this.unsupportedMessageBuilder, this.composerBuilder, this.chatMessageBuilder, this.chatAnimatedListBuilder, this.scrollToBottomBuilder, this.loadMoreBuilder, this.emptyChatListBuilder, this.linkPreviewBuilder}): super._(); + const _Builders({this.textMessageBuilder, this.textStreamMessageBuilder, this.imageMessageBuilder, this.fileMessageBuilder, this.videoMessageBuilder, this.audioMessageBuilder, this.systemMessageBuilder, this.customMessageBuilder, this.unsupportedMessageBuilder, this.composerBuilder, this.chatMessageBuilder, this.chatAnimatedListBuilder, this.scrollToBottomBuilder, this.loadMoreBuilder, this.emptyChatListBuilder, this.linkPreviewBuilder, this.reactionsBuilder}): super._(); /// Custom builder for text messages. @@ -143,6 +145,8 @@ class _Builders extends Builders { @override final EmptyChatListBuilder? emptyChatListBuilder; /// Custom builder for the link preview widget. @override final LinkPreviewBuilder? linkPreviewBuilder; +/// Custom builder for the reactions widget. +@override final ReactionsBuilder? reactionsBuilder; /// Create a copy of Builders /// with the given fields replaced by the non-null parameter values. @@ -154,16 +158,16 @@ _$BuildersCopyWith<_Builders> get copyWith => __$BuildersCopyWithImpl<_Builders> @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Builders&&(identical(other.textMessageBuilder, textMessageBuilder) || other.textMessageBuilder == textMessageBuilder)&&(identical(other.textStreamMessageBuilder, textStreamMessageBuilder) || other.textStreamMessageBuilder == textStreamMessageBuilder)&&(identical(other.imageMessageBuilder, imageMessageBuilder) || other.imageMessageBuilder == imageMessageBuilder)&&(identical(other.fileMessageBuilder, fileMessageBuilder) || other.fileMessageBuilder == fileMessageBuilder)&&(identical(other.videoMessageBuilder, videoMessageBuilder) || other.videoMessageBuilder == videoMessageBuilder)&&(identical(other.audioMessageBuilder, audioMessageBuilder) || other.audioMessageBuilder == audioMessageBuilder)&&(identical(other.systemMessageBuilder, systemMessageBuilder) || other.systemMessageBuilder == systemMessageBuilder)&&(identical(other.customMessageBuilder, customMessageBuilder) || other.customMessageBuilder == customMessageBuilder)&&(identical(other.unsupportedMessageBuilder, unsupportedMessageBuilder) || other.unsupportedMessageBuilder == unsupportedMessageBuilder)&&(identical(other.composerBuilder, composerBuilder) || other.composerBuilder == composerBuilder)&&(identical(other.chatMessageBuilder, chatMessageBuilder) || other.chatMessageBuilder == chatMessageBuilder)&&(identical(other.chatAnimatedListBuilder, chatAnimatedListBuilder) || other.chatAnimatedListBuilder == chatAnimatedListBuilder)&&(identical(other.scrollToBottomBuilder, scrollToBottomBuilder) || other.scrollToBottomBuilder == scrollToBottomBuilder)&&(identical(other.loadMoreBuilder, loadMoreBuilder) || other.loadMoreBuilder == loadMoreBuilder)&&(identical(other.emptyChatListBuilder, emptyChatListBuilder) || other.emptyChatListBuilder == emptyChatListBuilder)&&(identical(other.linkPreviewBuilder, linkPreviewBuilder) || other.linkPreviewBuilder == linkPreviewBuilder)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Builders&&(identical(other.textMessageBuilder, textMessageBuilder) || other.textMessageBuilder == textMessageBuilder)&&(identical(other.textStreamMessageBuilder, textStreamMessageBuilder) || other.textStreamMessageBuilder == textStreamMessageBuilder)&&(identical(other.imageMessageBuilder, imageMessageBuilder) || other.imageMessageBuilder == imageMessageBuilder)&&(identical(other.fileMessageBuilder, fileMessageBuilder) || other.fileMessageBuilder == fileMessageBuilder)&&(identical(other.videoMessageBuilder, videoMessageBuilder) || other.videoMessageBuilder == videoMessageBuilder)&&(identical(other.audioMessageBuilder, audioMessageBuilder) || other.audioMessageBuilder == audioMessageBuilder)&&(identical(other.systemMessageBuilder, systemMessageBuilder) || other.systemMessageBuilder == systemMessageBuilder)&&(identical(other.customMessageBuilder, customMessageBuilder) || other.customMessageBuilder == customMessageBuilder)&&(identical(other.unsupportedMessageBuilder, unsupportedMessageBuilder) || other.unsupportedMessageBuilder == unsupportedMessageBuilder)&&(identical(other.composerBuilder, composerBuilder) || other.composerBuilder == composerBuilder)&&(identical(other.chatMessageBuilder, chatMessageBuilder) || other.chatMessageBuilder == chatMessageBuilder)&&(identical(other.chatAnimatedListBuilder, chatAnimatedListBuilder) || other.chatAnimatedListBuilder == chatAnimatedListBuilder)&&(identical(other.scrollToBottomBuilder, scrollToBottomBuilder) || other.scrollToBottomBuilder == scrollToBottomBuilder)&&(identical(other.loadMoreBuilder, loadMoreBuilder) || other.loadMoreBuilder == loadMoreBuilder)&&(identical(other.emptyChatListBuilder, emptyChatListBuilder) || other.emptyChatListBuilder == emptyChatListBuilder)&&(identical(other.linkPreviewBuilder, linkPreviewBuilder) || other.linkPreviewBuilder == linkPreviewBuilder)&&(identical(other.reactionsBuilder, reactionsBuilder) || other.reactionsBuilder == reactionsBuilder)); } @override -int get hashCode => Object.hash(runtimeType,textMessageBuilder,textStreamMessageBuilder,imageMessageBuilder,fileMessageBuilder,videoMessageBuilder,audioMessageBuilder,systemMessageBuilder,customMessageBuilder,unsupportedMessageBuilder,composerBuilder,chatMessageBuilder,chatAnimatedListBuilder,scrollToBottomBuilder,loadMoreBuilder,emptyChatListBuilder,linkPreviewBuilder); +int get hashCode => Object.hash(runtimeType,textMessageBuilder,textStreamMessageBuilder,imageMessageBuilder,fileMessageBuilder,videoMessageBuilder,audioMessageBuilder,systemMessageBuilder,customMessageBuilder,unsupportedMessageBuilder,composerBuilder,chatMessageBuilder,chatAnimatedListBuilder,scrollToBottomBuilder,loadMoreBuilder,emptyChatListBuilder,linkPreviewBuilder,reactionsBuilder); @override String toString() { - return 'Builders(textMessageBuilder: $textMessageBuilder, textStreamMessageBuilder: $textStreamMessageBuilder, imageMessageBuilder: $imageMessageBuilder, fileMessageBuilder: $fileMessageBuilder, videoMessageBuilder: $videoMessageBuilder, audioMessageBuilder: $audioMessageBuilder, systemMessageBuilder: $systemMessageBuilder, customMessageBuilder: $customMessageBuilder, unsupportedMessageBuilder: $unsupportedMessageBuilder, composerBuilder: $composerBuilder, chatMessageBuilder: $chatMessageBuilder, chatAnimatedListBuilder: $chatAnimatedListBuilder, scrollToBottomBuilder: $scrollToBottomBuilder, loadMoreBuilder: $loadMoreBuilder, emptyChatListBuilder: $emptyChatListBuilder, linkPreviewBuilder: $linkPreviewBuilder)'; + return 'Builders(textMessageBuilder: $textMessageBuilder, textStreamMessageBuilder: $textStreamMessageBuilder, imageMessageBuilder: $imageMessageBuilder, fileMessageBuilder: $fileMessageBuilder, videoMessageBuilder: $videoMessageBuilder, audioMessageBuilder: $audioMessageBuilder, systemMessageBuilder: $systemMessageBuilder, customMessageBuilder: $customMessageBuilder, unsupportedMessageBuilder: $unsupportedMessageBuilder, composerBuilder: $composerBuilder, chatMessageBuilder: $chatMessageBuilder, chatAnimatedListBuilder: $chatAnimatedListBuilder, scrollToBottomBuilder: $scrollToBottomBuilder, loadMoreBuilder: $loadMoreBuilder, emptyChatListBuilder: $emptyChatListBuilder, linkPreviewBuilder: $linkPreviewBuilder, reactionsBuilder: $reactionsBuilder)'; } @@ -174,7 +178,7 @@ abstract mixin class _$BuildersCopyWith<$Res> implements $BuildersCopyWith<$Res> factory _$BuildersCopyWith(_Builders value, $Res Function(_Builders) _then) = __$BuildersCopyWithImpl; @override @useResult $Res call({ - TextMessageBuilder? textMessageBuilder, TextStreamMessageBuilder? textStreamMessageBuilder, ImageMessageBuilder? imageMessageBuilder, FileMessageBuilder? fileMessageBuilder, VideoMessageBuilder? videoMessageBuilder, AudioMessageBuilder? audioMessageBuilder, SystemMessageBuilder? systemMessageBuilder, CustomMessageBuilder? customMessageBuilder, UnsupportedMessageBuilder? unsupportedMessageBuilder, ComposerBuilder? composerBuilder, ChatMessageBuilder? chatMessageBuilder, ChatAnimatedListBuilder? chatAnimatedListBuilder, ScrollToBottomBuilder? scrollToBottomBuilder, LoadMoreBuilder? loadMoreBuilder, EmptyChatListBuilder? emptyChatListBuilder, LinkPreviewBuilder? linkPreviewBuilder + TextMessageBuilder? textMessageBuilder, TextStreamMessageBuilder? textStreamMessageBuilder, ImageMessageBuilder? imageMessageBuilder, FileMessageBuilder? fileMessageBuilder, VideoMessageBuilder? videoMessageBuilder, AudioMessageBuilder? audioMessageBuilder, SystemMessageBuilder? systemMessageBuilder, CustomMessageBuilder? customMessageBuilder, UnsupportedMessageBuilder? unsupportedMessageBuilder, ComposerBuilder? composerBuilder, ChatMessageBuilder? chatMessageBuilder, ChatAnimatedListBuilder? chatAnimatedListBuilder, ScrollToBottomBuilder? scrollToBottomBuilder, LoadMoreBuilder? loadMoreBuilder, EmptyChatListBuilder? emptyChatListBuilder, LinkPreviewBuilder? linkPreviewBuilder, ReactionsBuilder? reactionsBuilder }); @@ -191,7 +195,7 @@ class __$BuildersCopyWithImpl<$Res> /// Create a copy of Builders /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? textMessageBuilder = freezed,Object? textStreamMessageBuilder = freezed,Object? imageMessageBuilder = freezed,Object? fileMessageBuilder = freezed,Object? videoMessageBuilder = freezed,Object? audioMessageBuilder = freezed,Object? systemMessageBuilder = freezed,Object? customMessageBuilder = freezed,Object? unsupportedMessageBuilder = freezed,Object? composerBuilder = freezed,Object? chatMessageBuilder = freezed,Object? chatAnimatedListBuilder = freezed,Object? scrollToBottomBuilder = freezed,Object? loadMoreBuilder = freezed,Object? emptyChatListBuilder = freezed,Object? linkPreviewBuilder = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? textMessageBuilder = freezed,Object? textStreamMessageBuilder = freezed,Object? imageMessageBuilder = freezed,Object? fileMessageBuilder = freezed,Object? videoMessageBuilder = freezed,Object? audioMessageBuilder = freezed,Object? systemMessageBuilder = freezed,Object? customMessageBuilder = freezed,Object? unsupportedMessageBuilder = freezed,Object? composerBuilder = freezed,Object? chatMessageBuilder = freezed,Object? chatAnimatedListBuilder = freezed,Object? scrollToBottomBuilder = freezed,Object? loadMoreBuilder = freezed,Object? emptyChatListBuilder = freezed,Object? linkPreviewBuilder = freezed,Object? reactionsBuilder = freezed,}) { return _then(_Builders( textMessageBuilder: freezed == textMessageBuilder ? _self.textMessageBuilder : textMessageBuilder // ignore: cast_nullable_to_non_nullable as TextMessageBuilder?,textStreamMessageBuilder: freezed == textStreamMessageBuilder ? _self.textStreamMessageBuilder : textStreamMessageBuilder // ignore: cast_nullable_to_non_nullable @@ -209,7 +213,8 @@ as ChatAnimatedListBuilder?,scrollToBottomBuilder: freezed == scrollToBottomBuil as ScrollToBottomBuilder?,loadMoreBuilder: freezed == loadMoreBuilder ? _self.loadMoreBuilder : loadMoreBuilder // ignore: cast_nullable_to_non_nullable as LoadMoreBuilder?,emptyChatListBuilder: freezed == emptyChatListBuilder ? _self.emptyChatListBuilder : emptyChatListBuilder // ignore: cast_nullable_to_non_nullable as EmptyChatListBuilder?,linkPreviewBuilder: freezed == linkPreviewBuilder ? _self.linkPreviewBuilder : linkPreviewBuilder // ignore: cast_nullable_to_non_nullable -as LinkPreviewBuilder?, +as LinkPreviewBuilder?,reactionsBuilder: freezed == reactionsBuilder ? _self.reactionsBuilder : reactionsBuilder // ignore: cast_nullable_to_non_nullable +as ReactionsBuilder?, )); } diff --git a/packages/flutter_chat_core/lib/src/models/message.dart b/packages/flutter_chat_core/lib/src/models/message.dart index 79bce33c9..ecd5d158d 100644 --- a/packages/flutter_chat_core/lib/src/models/message.dart +++ b/packages/flutter_chat_core/lib/src/models/message.dart @@ -8,6 +8,8 @@ import 'link_preview_data.dart'; part 'message.freezed.dart'; part 'message.g.dart'; +typedef MessageReactions = Map>; + /// Base class for all message types. /// /// Uses a sealed class hierarchy with Freezed for immutability and union types. @@ -50,7 +52,7 @@ sealed class Message with _$Message { @EpochDateTimeConverter() DateTime? editedAt, /// Map of reaction keys to lists of user IDs who reacted. - Map>? reactions, + MessageReactions? reactions, /// Indicates if the message is pinned. bool? pinned, @@ -152,7 +154,7 @@ sealed class Message with _$Message { @EpochDateTimeConverter() DateTime? updatedAt, /// Map of reaction keys to lists of user IDs who reacted. - Map>? reactions, + MessageReactions? reactions, /// Indicates if the message is pinned. bool? pinned, @@ -222,7 +224,7 @@ sealed class Message with _$Message { @EpochDateTimeConverter() DateTime? updatedAt, /// Map of reaction keys to lists of user IDs who reacted. - Map>? reactions, + MessageReactions? reactions, /// Indicates if the message is pinned. bool? pinned, @@ -280,7 +282,7 @@ sealed class Message with _$Message { @EpochDateTimeConverter() DateTime? updatedAt, /// Map of reaction keys to lists of user IDs who reacted. - Map>? reactions, + MessageReactions? reactions, /// Indicates if the message is pinned. bool? pinned, @@ -344,7 +346,7 @@ sealed class Message with _$Message { @EpochDateTimeConverter() DateTime? updatedAt, /// Map of reaction keys to lists of user IDs who reacted. - Map>? reactions, + MessageReactions? reactions, /// Indicates if the message is pinned. bool? pinned, @@ -405,7 +407,7 @@ sealed class Message with _$Message { @EpochDateTimeConverter() DateTime? updatedAt, /// Map of reaction keys to lists of user IDs who reacted. - Map>? reactions, + MessageReactions? reactions, /// Indicates if the message is pinned. bool? pinned, @@ -454,7 +456,7 @@ sealed class Message with _$Message { @EpochDateTimeConverter() DateTime? updatedAt, /// Map of reaction keys to lists of user IDs who reacted. - Map>? reactions, + MessageReactions? reactions, /// Indicates if the message is pinned. bool? pinned, @@ -501,7 +503,7 @@ sealed class Message with _$Message { @EpochDateTimeConverter() DateTime? updatedAt, /// Map of reaction keys to lists of user IDs who reacted. - Map>? reactions, + MessageReactions? reactions, /// Indicates if the message is pinned. bool? pinned, diff --git a/packages/flutter_chat_core/lib/src/models/message.freezed.dart b/packages/flutter_chat_core/lib/src/models/message.freezed.dart index c360092a4..b8bcacad4 100644 --- a/packages/flutter_chat_core/lib/src/models/message.freezed.dart +++ b/packages/flutter_chat_core/lib/src/models/message.freezed.dart @@ -152,7 +152,7 @@ as MessageStatus?, @JsonSerializable() class TextMessage extends Message { - const TextMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, @EpochDateTimeConverter() this.editedAt, final Map>? reactions, this.pinned, final Map? metadata, this.status, required this.text, this.linkPreviewData, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'text',super._(); + const TextMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, @EpochDateTimeConverter() this.editedAt, final MessageReactions? reactions, this.pinned, final Map? metadata, this.status, required this.text, this.linkPreviewData, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'text',super._(); factory TextMessage.fromJson(Map json) => _$TextMessageFromJson(json); /// Unique identifier for the message. @@ -178,9 +178,9 @@ class TextMessage extends Message { /// Timestamp when the message was last edited. @EpochDateTimeConverter() final DateTime? editedAt; /// Map of reaction keys to lists of user IDs who reacted. - final Map>? _reactions; + final MessageReactions? _reactions; /// Map of reaction keys to lists of user IDs who reacted. -@override Map>? get reactions { +@override MessageReactions? get reactions { final value = _reactions; if (value == null) return null; if (_reactions is EqualUnmodifiableMapView) return _reactions; @@ -246,7 +246,7 @@ abstract mixin class $TextMessageCopyWith<$Res> implements $MessageCopyWith<$Res factory $TextMessageCopyWith(TextMessage value, $Res Function(TextMessage) _then) = _$TextMessageCopyWithImpl; @override @useResult $Res call({ - MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt,@EpochDateTimeConverter() DateTime? editedAt, Map>? reactions, bool? pinned, Map? metadata, MessageStatus? status, String text, LinkPreviewData? linkPreviewData + MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt,@EpochDateTimeConverter() DateTime? editedAt, MessageReactions? reactions, bool? pinned, Map? metadata, MessageStatus? status, String text, LinkPreviewData? linkPreviewData }); @@ -277,7 +277,7 @@ as DateTime?,seenAt: freezed == seenAt ? _self.seenAt : seenAt // ignore: cast_n as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable as DateTime?,reactions: freezed == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable -as Map>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as MessageReactions?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable as bool?,metadata: freezed == metadata ? _self._metadata : metadata // ignore: cast_nullable_to_non_nullable as Map?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as MessageStatus?,text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable @@ -440,7 +440,7 @@ as String, @JsonSerializable() class ImageMessage extends Message { - const ImageMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final Map>? reactions, this.pinned, final Map? metadata, this.status, required this.source, this.text, this.thumbhash, this.blurhash, this.width, this.height, this.size, this.hasOverlay, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'image',super._(); + const ImageMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final MessageReactions? reactions, this.pinned, final Map? metadata, this.status, required this.source, this.text, this.thumbhash, this.blurhash, this.width, this.height, this.size, this.hasOverlay, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'image',super._(); factory ImageMessage.fromJson(Map json) => _$ImageMessageFromJson(json); /// Unique identifier for the message. @@ -464,9 +464,9 @@ class ImageMessage extends Message { /// Timestamp when the message was last updated. @override@EpochDateTimeConverter() final DateTime? updatedAt; /// Map of reaction keys to lists of user IDs who reacted. - final Map>? _reactions; + final MessageReactions? _reactions; /// Map of reaction keys to lists of user IDs who reacted. -@override Map>? get reactions { +@override MessageReactions? get reactions { final value = _reactions; if (value == null) return null; if (_reactions is EqualUnmodifiableMapView) return _reactions; @@ -544,7 +544,7 @@ abstract mixin class $ImageMessageCopyWith<$Res> implements $MessageCopyWith<$Re factory $ImageMessageCopyWith(ImageMessage value, $Res Function(ImageMessage) _then) = _$ImageMessageCopyWithImpl; @override @useResult $Res call({ - MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, Map>? reactions, bool? pinned, Map? metadata, MessageStatus? status, String source, String? text, String? thumbhash, String? blurhash, double? width, double? height, int? size, bool? hasOverlay + MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, MessageReactions? reactions, bool? pinned, Map? metadata, MessageStatus? status, String source, String? text, String? thumbhash, String? blurhash, double? width, double? height, int? size, bool? hasOverlay }); @@ -574,7 +574,7 @@ as DateTime?,deliveredAt: freezed == deliveredAt ? _self.deliveredAt : delivered as DateTime?,seenAt: freezed == seenAt ? _self.seenAt : seenAt // ignore: cast_nullable_to_non_nullable as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?,reactions: freezed == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable -as Map>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as MessageReactions?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable as bool?,metadata: freezed == metadata ? _self._metadata : metadata // ignore: cast_nullable_to_non_nullable as Map?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as MessageStatus?,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable @@ -596,7 +596,7 @@ as bool?, @JsonSerializable() class FileMessage extends Message { - const FileMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final Map>? reactions, this.pinned, final Map? metadata, this.status, required this.source, required this.name, this.size, this.mimeType, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'file',super._(); + const FileMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final MessageReactions? reactions, this.pinned, final Map? metadata, this.status, required this.source, required this.name, this.size, this.mimeType, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'file',super._(); factory FileMessage.fromJson(Map json) => _$FileMessageFromJson(json); /// Unique identifier for the message. @@ -620,9 +620,9 @@ class FileMessage extends Message { /// Timestamp when the message was last updated. @override@EpochDateTimeConverter() final DateTime? updatedAt; /// Map of reaction keys to lists of user IDs who reacted. - final Map>? _reactions; + final MessageReactions? _reactions; /// Map of reaction keys to lists of user IDs who reacted. -@override Map>? get reactions { +@override MessageReactions? get reactions { final value = _reactions; if (value == null) return null; if (_reactions is EqualUnmodifiableMapView) return _reactions; @@ -692,7 +692,7 @@ abstract mixin class $FileMessageCopyWith<$Res> implements $MessageCopyWith<$Res factory $FileMessageCopyWith(FileMessage value, $Res Function(FileMessage) _then) = _$FileMessageCopyWithImpl; @override @useResult $Res call({ - MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, Map>? reactions, bool? pinned, Map? metadata, MessageStatus? status, String source, String name, int? size, String? mimeType + MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, MessageReactions? reactions, bool? pinned, Map? metadata, MessageStatus? status, String source, String name, int? size, String? mimeType }); @@ -722,7 +722,7 @@ as DateTime?,deliveredAt: freezed == deliveredAt ? _self.deliveredAt : delivered as DateTime?,seenAt: freezed == seenAt ? _self.seenAt : seenAt // ignore: cast_nullable_to_non_nullable as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?,reactions: freezed == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable -as Map>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as MessageReactions?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable as bool?,metadata: freezed == metadata ? _self._metadata : metadata // ignore: cast_nullable_to_non_nullable as Map?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as MessageStatus?,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable @@ -740,7 +740,7 @@ as String?, @JsonSerializable() class VideoMessage extends Message { - const VideoMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final Map>? reactions, this.pinned, final Map? metadata, this.status, required this.source, this.text, this.name, this.size, this.width, this.height, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'video',super._(); + const VideoMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final MessageReactions? reactions, this.pinned, final Map? metadata, this.status, required this.source, this.text, this.name, this.size, this.width, this.height, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'video',super._(); factory VideoMessage.fromJson(Map json) => _$VideoMessageFromJson(json); /// Unique identifier for the message. @@ -764,9 +764,9 @@ class VideoMessage extends Message { /// Timestamp when the message was last updated. @override@EpochDateTimeConverter() final DateTime? updatedAt; /// Map of reaction keys to lists of user IDs who reacted. - final Map>? _reactions; + final MessageReactions? _reactions; /// Map of reaction keys to lists of user IDs who reacted. -@override Map>? get reactions { +@override MessageReactions? get reactions { final value = _reactions; if (value == null) return null; if (_reactions is EqualUnmodifiableMapView) return _reactions; @@ -840,7 +840,7 @@ abstract mixin class $VideoMessageCopyWith<$Res> implements $MessageCopyWith<$Re factory $VideoMessageCopyWith(VideoMessage value, $Res Function(VideoMessage) _then) = _$VideoMessageCopyWithImpl; @override @useResult $Res call({ - MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, Map>? reactions, bool? pinned, Map? metadata, MessageStatus? status, String source, String? text, String? name, int? size, double? width, double? height + MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, MessageReactions? reactions, bool? pinned, Map? metadata, MessageStatus? status, String source, String? text, String? name, int? size, double? width, double? height }); @@ -870,7 +870,7 @@ as DateTime?,deliveredAt: freezed == deliveredAt ? _self.deliveredAt : delivered as DateTime?,seenAt: freezed == seenAt ? _self.seenAt : seenAt // ignore: cast_nullable_to_non_nullable as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?,reactions: freezed == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable -as Map>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as MessageReactions?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable as bool?,metadata: freezed == metadata ? _self._metadata : metadata // ignore: cast_nullable_to_non_nullable as Map?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as MessageStatus?,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable @@ -890,7 +890,7 @@ as double?, @JsonSerializable() class AudioMessage extends Message { - const AudioMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final Map>? reactions, this.pinned, final Map? metadata, this.status, required this.source, @DurationConverter() required this.duration, this.text, this.size, final List? waveform, final String? $type}): _reactions = reactions,_metadata = metadata,_waveform = waveform,$type = $type ?? 'audio',super._(); + const AudioMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final MessageReactions? reactions, this.pinned, final Map? metadata, this.status, required this.source, @DurationConverter() required this.duration, this.text, this.size, final List? waveform, final String? $type}): _reactions = reactions,_metadata = metadata,_waveform = waveform,$type = $type ?? 'audio',super._(); factory AudioMessage.fromJson(Map json) => _$AudioMessageFromJson(json); /// Unique identifier for the message. @@ -914,9 +914,9 @@ class AudioMessage extends Message { /// Timestamp when the message was last updated. @override@EpochDateTimeConverter() final DateTime? updatedAt; /// Map of reaction keys to lists of user IDs who reacted. - final Map>? _reactions; + final MessageReactions? _reactions; /// Map of reaction keys to lists of user IDs who reacted. -@override Map>? get reactions { +@override MessageReactions? get reactions { final value = _reactions; if (value == null) return null; if (_reactions is EqualUnmodifiableMapView) return _reactions; @@ -997,7 +997,7 @@ abstract mixin class $AudioMessageCopyWith<$Res> implements $MessageCopyWith<$Re factory $AudioMessageCopyWith(AudioMessage value, $Res Function(AudioMessage) _then) = _$AudioMessageCopyWithImpl; @override @useResult $Res call({ - MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, Map>? reactions, bool? pinned, Map? metadata, MessageStatus? status, String source,@DurationConverter() Duration duration, String? text, int? size, List? waveform + MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, MessageReactions? reactions, bool? pinned, Map? metadata, MessageStatus? status, String source,@DurationConverter() Duration duration, String? text, int? size, List? waveform }); @@ -1027,7 +1027,7 @@ as DateTime?,deliveredAt: freezed == deliveredAt ? _self.deliveredAt : delivered as DateTime?,seenAt: freezed == seenAt ? _self.seenAt : seenAt // ignore: cast_nullable_to_non_nullable as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?,reactions: freezed == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable -as Map>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as MessageReactions?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable as bool?,metadata: freezed == metadata ? _self._metadata : metadata // ignore: cast_nullable_to_non_nullable as Map?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as MessageStatus?,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable @@ -1046,7 +1046,7 @@ as List?, @JsonSerializable() class SystemMessage extends Message { - const SystemMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final Map>? reactions, this.pinned, final Map? metadata, this.status, required this.text, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'system',super._(); + const SystemMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final MessageReactions? reactions, this.pinned, final Map? metadata, this.status, required this.text, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'system',super._(); factory SystemMessage.fromJson(Map json) => _$SystemMessageFromJson(json); /// Unique identifier for the message. @@ -1070,9 +1070,9 @@ class SystemMessage extends Message { /// Timestamp when the message was last updated. @override@EpochDateTimeConverter() final DateTime? updatedAt; /// Map of reaction keys to lists of user IDs who reacted. - final Map>? _reactions; + final MessageReactions? _reactions; /// Map of reaction keys to lists of user IDs who reacted. -@override Map>? get reactions { +@override MessageReactions? get reactions { final value = _reactions; if (value == null) return null; if (_reactions is EqualUnmodifiableMapView) return _reactions; @@ -1136,7 +1136,7 @@ abstract mixin class $SystemMessageCopyWith<$Res> implements $MessageCopyWith<$R factory $SystemMessageCopyWith(SystemMessage value, $Res Function(SystemMessage) _then) = _$SystemMessageCopyWithImpl; @override @useResult $Res call({ - MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, Map>? reactions, bool? pinned, Map? metadata, MessageStatus? status, String text + MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, MessageReactions? reactions, bool? pinned, Map? metadata, MessageStatus? status, String text }); @@ -1166,7 +1166,7 @@ as DateTime?,deliveredAt: freezed == deliveredAt ? _self.deliveredAt : delivered as DateTime?,seenAt: freezed == seenAt ? _self.seenAt : seenAt // ignore: cast_nullable_to_non_nullable as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?,reactions: freezed == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable -as Map>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as MessageReactions?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable as bool?,metadata: freezed == metadata ? _self._metadata : metadata // ignore: cast_nullable_to_non_nullable as Map?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as MessageStatus?,text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable @@ -1181,7 +1181,7 @@ as String, @JsonSerializable() class CustomMessage extends Message { - const CustomMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final Map>? reactions, this.pinned, final Map? metadata, this.status, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'custom',super._(); + const CustomMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final MessageReactions? reactions, this.pinned, final Map? metadata, this.status, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'custom',super._(); factory CustomMessage.fromJson(Map json) => _$CustomMessageFromJson(json); /// Unique identifier for the message. @@ -1205,9 +1205,9 @@ class CustomMessage extends Message { /// Timestamp when the message was last updated. @override@EpochDateTimeConverter() final DateTime? updatedAt; /// Map of reaction keys to lists of user IDs who reacted. - final Map>? _reactions; + final MessageReactions? _reactions; /// Map of reaction keys to lists of user IDs who reacted. -@override Map>? get reactions { +@override MessageReactions? get reactions { final value = _reactions; if (value == null) return null; if (_reactions is EqualUnmodifiableMapView) return _reactions; @@ -1269,7 +1269,7 @@ abstract mixin class $CustomMessageCopyWith<$Res> implements $MessageCopyWith<$R factory $CustomMessageCopyWith(CustomMessage value, $Res Function(CustomMessage) _then) = _$CustomMessageCopyWithImpl; @override @useResult $Res call({ - MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, Map>? reactions, bool? pinned, Map? metadata, MessageStatus? status + MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, MessageReactions? reactions, bool? pinned, Map? metadata, MessageStatus? status }); @@ -1299,7 +1299,7 @@ as DateTime?,deliveredAt: freezed == deliveredAt ? _self.deliveredAt : delivered as DateTime?,seenAt: freezed == seenAt ? _self.seenAt : seenAt // ignore: cast_nullable_to_non_nullable as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?,reactions: freezed == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable -as Map>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as MessageReactions?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable as bool?,metadata: freezed == metadata ? _self._metadata : metadata // ignore: cast_nullable_to_non_nullable as Map?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as MessageStatus?, @@ -1313,7 +1313,7 @@ as MessageStatus?, @JsonSerializable() class UnsupportedMessage extends Message { - const UnsupportedMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final Map>? reactions, this.pinned, final Map? metadata, this.status, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'unsupported',super._(); + const UnsupportedMessage({required this.id, required this.authorId, this.replyToMessageId, @EpochDateTimeConverter() this.createdAt, @EpochDateTimeConverter() this.deletedAt, @EpochDateTimeConverter() this.failedAt, @EpochDateTimeConverter() this.sentAt, @EpochDateTimeConverter() this.deliveredAt, @EpochDateTimeConverter() this.seenAt, @EpochDateTimeConverter() this.updatedAt, final MessageReactions? reactions, this.pinned, final Map? metadata, this.status, final String? $type}): _reactions = reactions,_metadata = metadata,$type = $type ?? 'unsupported',super._(); factory UnsupportedMessage.fromJson(Map json) => _$UnsupportedMessageFromJson(json); /// Unique identifier for the message. @@ -1337,9 +1337,9 @@ class UnsupportedMessage extends Message { /// Timestamp when the message was last updated. @override@EpochDateTimeConverter() final DateTime? updatedAt; /// Map of reaction keys to lists of user IDs who reacted. - final Map>? _reactions; + final MessageReactions? _reactions; /// Map of reaction keys to lists of user IDs who reacted. -@override Map>? get reactions { +@override MessageReactions? get reactions { final value = _reactions; if (value == null) return null; if (_reactions is EqualUnmodifiableMapView) return _reactions; @@ -1401,7 +1401,7 @@ abstract mixin class $UnsupportedMessageCopyWith<$Res> implements $MessageCopyWi factory $UnsupportedMessageCopyWith(UnsupportedMessage value, $Res Function(UnsupportedMessage) _then) = _$UnsupportedMessageCopyWithImpl; @override @useResult $Res call({ - MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, Map>? reactions, bool? pinned, Map? metadata, MessageStatus? status + MessageID id, UserID authorId, MessageID? replyToMessageId,@EpochDateTimeConverter() DateTime? createdAt,@EpochDateTimeConverter() DateTime? deletedAt,@EpochDateTimeConverter() DateTime? failedAt,@EpochDateTimeConverter() DateTime? sentAt,@EpochDateTimeConverter() DateTime? deliveredAt,@EpochDateTimeConverter() DateTime? seenAt,@EpochDateTimeConverter() DateTime? updatedAt, MessageReactions? reactions, bool? pinned, Map? metadata, MessageStatus? status }); @@ -1431,7 +1431,7 @@ as DateTime?,deliveredAt: freezed == deliveredAt ? _self.deliveredAt : delivered as DateTime?,seenAt: freezed == seenAt ? _self.seenAt : seenAt // ignore: cast_nullable_to_non_nullable as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?,reactions: freezed == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable -as Map>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as MessageReactions?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable as bool?,metadata: freezed == metadata ? _self._metadata : metadata // ignore: cast_nullable_to_non_nullable as Map?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as MessageStatus?, diff --git a/packages/flutter_chat_core/lib/src/theme/chat_theme.dart b/packages/flutter_chat_core/lib/src/theme/chat_theme.dart index 9cfcab45f..ae4561d66 100644 --- a/packages/flutter_chat_core/lib/src/theme/chat_theme.dart +++ b/packages/flutter_chat_core/lib/src/theme/chat_theme.dart @@ -85,6 +85,9 @@ abstract class ChatColors with _$ChatColors { /// A slightly lighter/darker variant of [surfaceContainer]. required Color surfaceContainerHigh, + + /// The highest/most elevated container surface. + required Color surfaceContainerHighest, }) = _ChatColors; const ChatColors._(); @@ -98,6 +101,7 @@ abstract class ChatColors with _$ChatColors { surfaceContainerLow: Color(0xfffafafa), surfaceContainer: Color(0xfff5f5f5), surfaceContainerHigh: Color(0xffeeeeee), + surfaceContainerHighest: Color(0xfff0f0f0), ); /// Default dark color palette. @@ -109,6 +113,7 @@ abstract class ChatColors with _$ChatColors { surfaceContainerLow: Color(0xff121212), surfaceContainer: Color(0xff1c1c1c), surfaceContainerHigh: Color(0xff242424), + surfaceContainerHighest: Color(0xff444444), ); /// Creates [ChatColors] from a Material [ThemeData]. @@ -120,6 +125,7 @@ abstract class ChatColors with _$ChatColors { surfaceContainerLow: themeData.colorScheme.surfaceContainerLow, surfaceContainer: themeData.colorScheme.surfaceContainer, surfaceContainerHigh: themeData.colorScheme.surfaceContainerHigh, + surfaceContainerHighest: themeData.colorScheme.surfaceContainerHighest, ); /// Merges this color scheme with another [ChatColors]. @@ -135,6 +141,7 @@ abstract class ChatColors with _$ChatColors { surfaceContainerLow: other.surfaceContainerLow, surfaceContainer: other.surfaceContainer, surfaceContainerHigh: other.surfaceContainerHigh, + surfaceContainerHighest: other.surfaceContainerHighest, ); } } diff --git a/packages/flutter_chat_core/lib/src/theme/chat_theme.freezed.dart b/packages/flutter_chat_core/lib/src/theme/chat_theme.freezed.dart index 87c219960..a67d129a0 100644 --- a/packages/flutter_chat_core/lib/src/theme/chat_theme.freezed.dart +++ b/packages/flutter_chat_core/lib/src/theme/chat_theme.freezed.dart @@ -197,7 +197,8 @@ mixin _$ChatColors { Color get onSurface;/// Background color for elements like received messages. Color get surfaceContainer;/// A slightly lighter/darker variant of [surfaceContainer]. Color get surfaceContainerLow;/// A slightly lighter/darker variant of [surfaceContainer]. - Color get surfaceContainerHigh; + Color get surfaceContainerHigh;/// The highest/most elevated container surface. + Color get surfaceContainerHighest; /// Create a copy of ChatColors /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -208,16 +209,16 @@ $ChatColorsCopyWith get copyWith => _$ChatColorsCopyWithImpl Object.hash(runtimeType,primary,onPrimary,surface,onSurface,surfaceContainer,surfaceContainerLow,surfaceContainerHigh); +int get hashCode => Object.hash(runtimeType,primary,onPrimary,surface,onSurface,surfaceContainer,surfaceContainerLow,surfaceContainerHigh,surfaceContainerHighest); @override String toString() { - return 'ChatColors(primary: $primary, onPrimary: $onPrimary, surface: $surface, onSurface: $onSurface, surfaceContainer: $surfaceContainer, surfaceContainerLow: $surfaceContainerLow, surfaceContainerHigh: $surfaceContainerHigh)'; + return 'ChatColors(primary: $primary, onPrimary: $onPrimary, surface: $surface, onSurface: $onSurface, surfaceContainer: $surfaceContainer, surfaceContainerLow: $surfaceContainerLow, surfaceContainerHigh: $surfaceContainerHigh, surfaceContainerHighest: $surfaceContainerHighest)'; } @@ -228,7 +229,7 @@ abstract mixin class $ChatColorsCopyWith<$Res> { factory $ChatColorsCopyWith(ChatColors value, $Res Function(ChatColors) _then) = _$ChatColorsCopyWithImpl; @useResult $Res call({ - Color primary, Color onPrimary, Color surface, Color onSurface, Color surfaceContainer, Color surfaceContainerLow, Color surfaceContainerHigh + Color primary, Color onPrimary, Color surface, Color onSurface, Color surfaceContainer, Color surfaceContainerLow, Color surfaceContainerHigh, Color surfaceContainerHighest }); @@ -245,7 +246,7 @@ class _$ChatColorsCopyWithImpl<$Res> /// Create a copy of ChatColors /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? primary = null,Object? onPrimary = null,Object? surface = null,Object? onSurface = null,Object? surfaceContainer = null,Object? surfaceContainerLow = null,Object? surfaceContainerHigh = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? primary = null,Object? onPrimary = null,Object? surface = null,Object? onSurface = null,Object? surfaceContainer = null,Object? surfaceContainerLow = null,Object? surfaceContainerHigh = null,Object? surfaceContainerHighest = null,}) { return _then(_self.copyWith( primary: null == primary ? _self.primary : primary // ignore: cast_nullable_to_non_nullable as Color,onPrimary: null == onPrimary ? _self.onPrimary : onPrimary // ignore: cast_nullable_to_non_nullable @@ -254,6 +255,7 @@ as Color,onSurface: null == onSurface ? _self.onSurface : onSurface // ignore: c as Color,surfaceContainer: null == surfaceContainer ? _self.surfaceContainer : surfaceContainer // ignore: cast_nullable_to_non_nullable as Color,surfaceContainerLow: null == surfaceContainerLow ? _self.surfaceContainerLow : surfaceContainerLow // ignore: cast_nullable_to_non_nullable as Color,surfaceContainerHigh: null == surfaceContainerHigh ? _self.surfaceContainerHigh : surfaceContainerHigh // ignore: cast_nullable_to_non_nullable +as Color,surfaceContainerHighest: null == surfaceContainerHighest ? _self.surfaceContainerHighest : surfaceContainerHighest // ignore: cast_nullable_to_non_nullable as Color, )); } @@ -265,7 +267,7 @@ as Color, class _ChatColors extends ChatColors { - const _ChatColors({required this.primary, required this.onPrimary, required this.surface, required this.onSurface, required this.surfaceContainer, required this.surfaceContainerLow, required this.surfaceContainerHigh}): super._(); + const _ChatColors({required this.primary, required this.onPrimary, required this.surface, required this.onSurface, required this.surfaceContainer, required this.surfaceContainerLow, required this.surfaceContainerHigh, required this.surfaceContainerHighest}): super._(); /// Primary color, often used for sent messages and accents. @@ -282,6 +284,8 @@ class _ChatColors extends ChatColors { @override final Color surfaceContainerLow; /// A slightly lighter/darker variant of [surfaceContainer]. @override final Color surfaceContainerHigh; +/// The highest/most elevated container surface. +@override final Color surfaceContainerHighest; /// Create a copy of ChatColors /// with the given fields replaced by the non-null parameter values. @@ -293,16 +297,16 @@ _$ChatColorsCopyWith<_ChatColors> get copyWith => __$ChatColorsCopyWithImpl<_Cha @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatColors&&(identical(other.primary, primary) || other.primary == primary)&&(identical(other.onPrimary, onPrimary) || other.onPrimary == onPrimary)&&(identical(other.surface, surface) || other.surface == surface)&&(identical(other.onSurface, onSurface) || other.onSurface == onSurface)&&(identical(other.surfaceContainer, surfaceContainer) || other.surfaceContainer == surfaceContainer)&&(identical(other.surfaceContainerLow, surfaceContainerLow) || other.surfaceContainerLow == surfaceContainerLow)&&(identical(other.surfaceContainerHigh, surfaceContainerHigh) || other.surfaceContainerHigh == surfaceContainerHigh)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatColors&&(identical(other.primary, primary) || other.primary == primary)&&(identical(other.onPrimary, onPrimary) || other.onPrimary == onPrimary)&&(identical(other.surface, surface) || other.surface == surface)&&(identical(other.onSurface, onSurface) || other.onSurface == onSurface)&&(identical(other.surfaceContainer, surfaceContainer) || other.surfaceContainer == surfaceContainer)&&(identical(other.surfaceContainerLow, surfaceContainerLow) || other.surfaceContainerLow == surfaceContainerLow)&&(identical(other.surfaceContainerHigh, surfaceContainerHigh) || other.surfaceContainerHigh == surfaceContainerHigh)&&(identical(other.surfaceContainerHighest, surfaceContainerHighest) || other.surfaceContainerHighest == surfaceContainerHighest)); } @override -int get hashCode => Object.hash(runtimeType,primary,onPrimary,surface,onSurface,surfaceContainer,surfaceContainerLow,surfaceContainerHigh); +int get hashCode => Object.hash(runtimeType,primary,onPrimary,surface,onSurface,surfaceContainer,surfaceContainerLow,surfaceContainerHigh,surfaceContainerHighest); @override String toString() { - return 'ChatColors(primary: $primary, onPrimary: $onPrimary, surface: $surface, onSurface: $onSurface, surfaceContainer: $surfaceContainer, surfaceContainerLow: $surfaceContainerLow, surfaceContainerHigh: $surfaceContainerHigh)'; + return 'ChatColors(primary: $primary, onPrimary: $onPrimary, surface: $surface, onSurface: $onSurface, surfaceContainer: $surfaceContainer, surfaceContainerLow: $surfaceContainerLow, surfaceContainerHigh: $surfaceContainerHigh, surfaceContainerHighest: $surfaceContainerHighest)'; } @@ -313,7 +317,7 @@ abstract mixin class _$ChatColorsCopyWith<$Res> implements $ChatColorsCopyWith<$ factory _$ChatColorsCopyWith(_ChatColors value, $Res Function(_ChatColors) _then) = __$ChatColorsCopyWithImpl; @override @useResult $Res call({ - Color primary, Color onPrimary, Color surface, Color onSurface, Color surfaceContainer, Color surfaceContainerLow, Color surfaceContainerHigh + Color primary, Color onPrimary, Color surface, Color onSurface, Color surfaceContainer, Color surfaceContainerLow, Color surfaceContainerHigh, Color surfaceContainerHighest }); @@ -330,7 +334,7 @@ class __$ChatColorsCopyWithImpl<$Res> /// Create a copy of ChatColors /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? primary = null,Object? onPrimary = null,Object? surface = null,Object? onSurface = null,Object? surfaceContainer = null,Object? surfaceContainerLow = null,Object? surfaceContainerHigh = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? primary = null,Object? onPrimary = null,Object? surface = null,Object? onSurface = null,Object? surfaceContainer = null,Object? surfaceContainerLow = null,Object? surfaceContainerHigh = null,Object? surfaceContainerHighest = null,}) { return _then(_ChatColors( primary: null == primary ? _self.primary : primary // ignore: cast_nullable_to_non_nullable as Color,onPrimary: null == onPrimary ? _self.onPrimary : onPrimary // ignore: cast_nullable_to_non_nullable @@ -339,6 +343,7 @@ as Color,onSurface: null == onSurface ? _self.onSurface : onSurface // ignore: c as Color,surfaceContainer: null == surfaceContainer ? _self.surfaceContainer : surfaceContainer // ignore: cast_nullable_to_non_nullable as Color,surfaceContainerLow: null == surfaceContainerLow ? _self.surfaceContainerLow : surfaceContainerLow // ignore: cast_nullable_to_non_nullable as Color,surfaceContainerHigh: null == surfaceContainerHigh ? _self.surfaceContainerHigh : surfaceContainerHigh // ignore: cast_nullable_to_non_nullable +as Color,surfaceContainerHighest: null == surfaceContainerHighest ? _self.surfaceContainerHighest : surfaceContainerHighest // ignore: cast_nullable_to_non_nullable as Color, )); } diff --git a/packages/flutter_chat_ui/lib/flutter_chat_ui.dart b/packages/flutter_chat_ui/lib/flutter_chat_ui.dart index 9dcc05e59..db5c60a85 100644 --- a/packages/flutter_chat_ui/lib/flutter_chat_ui.dart +++ b/packages/flutter_chat_ui/lib/flutter_chat_ui.dart @@ -6,6 +6,7 @@ export 'src/chat.dart'; export 'src/chat_animated_list/chat_animated_list.dart'; export 'src/chat_animated_list/chat_animated_list_reversed.dart'; export 'src/chat_message/chat_message.dart'; +export 'src/chat_message/chat_message_build_helpers.dart'; export 'src/composer.dart'; export 'src/empty_chat_list.dart'; export 'src/is_typing.dart'; @@ -13,6 +14,7 @@ export 'src/load_more.dart'; export 'src/scroll_to_bottom.dart'; export 'src/simple_text_message.dart'; export 'src/username.dart'; +export 'src/utils/chat_providers.dart'; export 'src/utils/composer_height_notifier.dart'; export 'src/utils/load_more_notifier.dart'; export 'src/utils/typedefs.dart'; diff --git a/packages/flutter_chat_ui/lib/src/chat.dart b/packages/flutter_chat_ui/lib/src/chat.dart index a30a005e1..2b1f5f22a 100644 --- a/packages/flutter_chat_ui/lib/src/chat.dart +++ b/packages/flutter_chat_ui/lib/src/chat.dart @@ -134,6 +134,8 @@ class _ChatState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { + /// IMPORTANT: Keep this list in sync with the MultiProvider helper in [ChatProviders]]. + return MultiProvider( providers: [ Provider.value(value: widget.currentUserId), 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 0c28bbd48..29a450cbd 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 @@ -134,6 +134,10 @@ class ChatMessage extends StatelessWidget { final resolvedPadding = padding ?? _resolveDefaultPadding(context); + final reactionsBuilder = context.read()?.reactionsBuilder; + Widget? reactionsWidget; + reactionsWidget = reactionsBuilder?.call(context, message, isSentByMe); + final Widget messageWidget = Column( mainAxisSize: MainAxisSize.min, children: [ @@ -147,22 +151,32 @@ class ChatMessage extends StatelessWidget { ), ), GestureDetector( - onTapUp: - (details) => onMessageTap?.call( - context, - message, - index: index, - details: details, - ), + onTapUp: (details) { + onMessageTap?.call( + context, + message, + index: index, + details: details, + isSentByMe: isSentByMe, + ); + }, onDoubleTap: - () => onMessageDoubleTap?.call(context, message, index: index), - onLongPressStart: - (details) => onMessageLongPress?.call( + () => onMessageDoubleTap?.call( context, message, index: index, - details: details, + isSentByMe: isSentByMe, ), + onLongPressStart: (details) { + onMessageLongPress?.call( + context, + message, + index: index, + details: details, + isSentByMe: isSentByMe, + ); + return; + }, child: FadeTransition( opacity: curvedAnimation, child: SizeTransition( @@ -180,7 +194,10 @@ class ChatMessage extends StatelessWidget { (isSentByMe ? sentMessageAlignment : receivedMessageAlignment), - child: _buildMessage(isSentByMe: isSentByMe), + child: _buildMessage( + isSentByMe: isSentByMe, + reactionsWidget: reactionsWidget, + ), ), ), ), @@ -203,27 +220,49 @@ class ChatMessage extends StatelessWidget { return messageWidget; } - Widget _buildMessage({required bool isSentByMe}) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - isSentByMe - ? sentMessageColumnAlignment - : receivedMessageColumnAlignment, - children: [ - if (topWidget != null) topWidget!, - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - isSentByMe ? sentMessageRowAlignment : receivedMessageRowAlignment, - children: [ - if (leadingWidget != null) leadingWidget!, - Flexible(child: child), - if (trailingWidget != null) trailingWidget!, - ], - ), - if (bottomWidget != null) bottomWidget!, - ], - ); + Widget _buildMessage({required bool isSentByMe, Widget? reactionsWidget}) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + isSentByMe + ? sentMessageColumnAlignment + : receivedMessageColumnAlignment, + children: [ + if (topWidget != null) topWidget!, + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + isSentByMe + ? sentMessageRowAlignment + : receivedMessageRowAlignment, + children: [ + if (leadingWidget != null) leadingWidget!, + Flexible( + child: + reactionsWidget != null + ? Stack( + children: [ + // TODO Find better way to add height for the reactions widget + // TODO: maybe we could set a width to allow at least some space for the reactions widget + // We message is really short ? + Column(children: [child, SizedBox(height: 16)]), + Positioned( + bottom: 0, + left: 8, + right: 8, + child: reactionsWidget, + ), + ], + ) + : child, + ), + if (trailingWidget != null) trailingWidget!, + ], + ), + if (bottomWidget != null) bottomWidget!, + ], + ); + } EdgeInsetsGeometry _resolveDefaultPadding(BuildContext context) { if (index == 0) { diff --git a/packages/flutter_chat_ui/lib/src/chat_message/chat_message_build_helpers.dart b/packages/flutter_chat_ui/lib/src/chat_message/chat_message_build_helpers.dart new file mode 100644 index 000000000..57e28c15f --- /dev/null +++ b/packages/flutter_chat_ui/lib/src/chat_message/chat_message_build_helpers.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; + +import '../simple_text_message.dart'; + +Widget buildMessageContent( + BuildContext context, + Builders builders, + Message message, + int index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) { + switch (message) { + case TextMessage(): + return builders.textMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + SimpleTextMessage(message: message, index: index); + case TextStreamMessage(): + return builders.textStreamMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + const SizedBox.shrink(); + case ImageMessage(): + final result = + builders.imageMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + const SizedBox.shrink(); + assert( + !(result is SizedBox && result.width == 0 && result.height == 0), + 'You are trying to display an image message but you have not provided an imageMessageBuilder. ' + 'Use builders parameter of Chat widget to provide an image message widget. ' + 'If you want to use default image message widget, install flyer_chat_image_message package and use FlyerChatImageMessage widget.', + ); + return result; + case FileMessage(): + return builders.fileMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + const SizedBox.shrink(); + case VideoMessage(): + return builders.videoMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + const SizedBox.shrink(); + case AudioMessage(): + return builders.audioMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + const SizedBox.shrink(); + case SystemMessage(): + return builders.systemMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + const SizedBox.shrink(); + case CustomMessage(): + return builders.customMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + const SizedBox.shrink(); + case UnsupportedMessage(): + return builders.unsupportedMessageBuilder?.call( + context, + message, + index, + isSentByMe: isSentByMe, + groupStatus: groupStatus, + ) ?? + const Text( + 'This message is not supported. Please update your app.', + ); + } + } \ No newline at end of file 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 dea1b7a1c..e55d6f94d 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 @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:provider/provider.dart'; -import '../simple_text_message.dart'; import 'chat_message.dart'; +import 'chat_message_build_helpers.dart'; /// Internal widget responsible for building and updating a single chat message item. /// @@ -99,7 +99,7 @@ class _ChatMessageInternalState extends State { final groupStatus = _resolveGroupStatus(context); - final child = _buildMessage( + final child = buildMessageContent( context, builders, _updatedMessage, @@ -182,109 +182,6 @@ class _ChatMessageInternalState extends State { return null; } } - - Widget _buildMessage( - BuildContext context, - Builders builders, - Message message, - int index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) { - switch (message) { - case TextMessage(): - return builders.textMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - SimpleTextMessage(message: message, index: index); - case TextStreamMessage(): - return builders.textStreamMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - const SizedBox.shrink(); - case ImageMessage(): - final result = - builders.imageMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - const SizedBox.shrink(); - assert( - !(result is SizedBox && result.width == 0 && result.height == 0), - 'You are trying to display an image message but you have not provided an imageMessageBuilder. ' - 'Use builders parameter of Chat widget to provide an image message widget. ' - 'If you want to use default image message widget, install flyer_chat_image_message package and use FlyerChatImageMessage widget.', - ); - return result; - case FileMessage(): - return builders.fileMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - const SizedBox.shrink(); - case VideoMessage(): - return builders.videoMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - const SizedBox.shrink(); - case AudioMessage(): - return builders.audioMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - const SizedBox.shrink(); - case SystemMessage(): - return builders.systemMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - const SizedBox.shrink(); - case CustomMessage(): - return builders.customMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - const SizedBox.shrink(); - case UnsupportedMessage(): - return builders.unsupportedMessageBuilder?.call( - context, - message, - index, - isSentByMe: isSentByMe, - groupStatus: groupStatus, - ) ?? - const Text( - 'This message is not supported. Please update your app.', - ); - } - } } /// Determines if two messages should be grouped together based on the grouping mode. diff --git a/packages/flutter_chat_ui/lib/src/utils/chat_providers.dart b/packages/flutter_chat_ui/lib/src/utils/chat_providers.dart new file mode 100644 index 000000000..0de982118 --- /dev/null +++ b/packages/flutter_chat_ui/lib/src/utils/chat_providers.dart @@ -0,0 +1,64 @@ +import 'package:cross_cache/cross_cache.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; + +import '../chat.dart'; +import 'composer_height_notifier.dart'; +import 'load_more_notifier.dart'; +import 'typedefs.dart'; + +/// A utility class to re-expose the current Chat-related providers +/// for use in dialogs, custom routes, or overlays. +/// +/// This ensures that context-dependent widgets (e.g., using `context.watch`) +/// work properly in a new widget tree. +/// +/// +/// IMPORTANT: Keep this list in sync with the main MultiProvider in [Chat]]. + +class ChatProviders { + /// Recreates the list of `Provider`s from the current [context], + /// so you can rewrap a new widget subtree (e.g. in a Hero route or dialog). + static List from(BuildContext context) => [ + Provider.value(value: mustRead(context)), + Provider.value(value: mustRead(context)), + Provider.value(value: mustRead(context)), + Provider.value(value: mustRead(context)), + Provider.value(value: mustRead(context)), + Provider.value(value: mustRead(context)), + ChangeNotifierProvider.value(value: mustRead(context)), + Provider.value(value: mustRead(context)), + + // Optional callbacks use context.read() directly: + Provider.value(value: context.read()), + Provider.value(value: context.read()), + Provider.value(value: context.read()), + Provider.value(value: context.read()), + + ChangeNotifierProvider.value( + value: mustRead(context), + ), + ChangeNotifierProvider.value(value: mustRead(context)), + ]; +} + +/// Safely reads a provider from the given [context]. +/// Throws a clear error if the provider is missing, +/// instead of silently crashing at runtime. +T mustRead(BuildContext context) { + try { + return context.read(); + } catch (e, stack) { + throw FlutterError.fromParts([ + ErrorSummary('Missing provider for type $T'), + ErrorDescription( + 'ChatProviders.from(context) tried to read a $T, but it was not found in the widget tree.', + ), + ErrorHint('Ensure this provider is available in the current context.'), + DiagnosticsStackTrace('Stack trace', stack), + ]); + } +} diff --git a/packages/flutter_chat_ui/lib/src/utils/typedefs.dart b/packages/flutter_chat_ui/lib/src/utils/typedefs.dart index f7b645e40..902418fa0 100644 --- a/packages/flutter_chat_ui/lib/src/utils/typedefs.dart +++ b/packages/flutter_chat_ui/lib/src/utils/typedefs.dart @@ -10,13 +10,19 @@ typedef OnMessageTapCallback = Message message, { int index, TapUpDetails details, + required bool isSentByMe, }); /// Callback signature for when a message is double tapped. /// [context] is the BuildContext from the widget tree where the tap occurs. /// Provides the tapped [message], its [index] typedef OnMessageDoubleTapCallback = - void Function(BuildContext context, Message message, {int index}); + void Function( + BuildContext context, + Message message, { + int index, + required bool isSentByMe, + }); /// Callback signature for when a message is long-pressed. /// [context] is the BuildContext from the widget tree where the long press occurs. @@ -27,6 +33,7 @@ typedef OnMessageLongPressCallback = Message message, { int index, LongPressStartDetails details, + required bool isSentByMe, }); /// Callback signature for when the user attempts to send a message. diff --git a/packages/flyer_chat_reactions/LICENSE b/packages/flyer_chat_reactions/LICENSE new file mode 100644 index 000000000..5a17b5947 --- /dev/null +++ b/packages/flyer_chat_reactions/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Oleksandr Demchenko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/flyer_chat_reactions/analysis_options.yaml b/packages/flyer_chat_reactions/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/packages/flyer_chat_reactions/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/packages/flyer_chat_reactions/lib/flyer_chat_reactions.dart b/packages/flyer_chat_reactions/lib/flyer_chat_reactions.dart new file mode 100644 index 000000000..b72bad667 --- /dev/null +++ b/packages/flyer_chat_reactions/lib/flyer_chat_reactions.dart @@ -0,0 +1,5 @@ +export 'src/models/reaction.dart'; +export 'src/widgets/flyer_chat_reactions_row.dart'; +export 'src/widgets/reaction_tile.dart'; +export 'src/widgets/reactions_dialog.dart'; +export 'src/widgets/reactions_list.dart'; diff --git a/packages/flyer_chat_reactions/lib/src/helpers/chat_theme_extensions.dart b/packages/flyer_chat_reactions/lib/src/helpers/chat_theme_extensions.dart new file mode 100644 index 000000000..3184e34bd --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/helpers/chat_theme_extensions.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; + +extension ReactionsTheme on ChatTheme { + Color get reactionBackgroundColor => colors.surfaceContainer; + Color get reactionReactedBackgroundColor => colors.surfaceContainerHighest; + Color get reactionBorderColor => colors.surface; + + Color get reactionCountTextColor => colors.onSurface; + TextStyle get reactionEmojiTextStyle => typography.bodyMedium; + TextStyle get reactionCountTextStyle => + typography.bodySmall.copyWith(fontWeight: FontWeight.bold); + TextStyle get reactionSurplusTextStyle => + typography.bodySmall.copyWith(fontWeight: FontWeight.bold); +} diff --git a/packages/flyer_chat_reactions/lib/src/models/default_data.dart b/packages/flyer_chat_reactions/lib/src/models/default_data.dart new file mode 100644 index 000000000..0076d48ce --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/models/default_data.dart @@ -0,0 +1,4 @@ +class DefaultData { + // / Default list of reactions to be displayed from emojis + static const List reactions = ['👍', '❤️', '😂', '😮', '😢', '😠']; +} diff --git a/packages/flyer_chat_reactions/lib/src/models/reaction.dart b/packages/flyer_chat_reactions/lib/src/models/reaction.dart new file mode 100644 index 000000000..55ade8dac --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/models/reaction.dart @@ -0,0 +1,60 @@ +import 'package:flutter_chat_core/flutter_chat_core.dart'; + +class Reaction { + final String emoji; + final bool isReactedByUser; + final int count; + final List userIds; + + Reaction({ + required this.emoji, + required this.count, + required this.isReactedByUser, + required this.userIds, + }); + + @override + String toString() { + return 'Reaction(emoji: $emoji, count: $count, isReactedByUser: $isReactedByUser, userIds: $userIds)'; + } +} + +/// Converts a map of reactions to a list of [Reaction] objects +/// +/// [reactions] is a map [MessageReactions] where keys are emoji strings and values are lists of user IDs +/// [currentUserId] is used to determine if the current user has reacted +List reactionsFromMessageReactions({ + required MessageReactions? reactions, + required String currentUserId, +}) { + if (reactions == null) { + return []; + } + return reactions.entries.map((entry) { + final emoji = entry.key; + final users = entry.value; + return Reaction( + emoji: emoji, + count: users.length, + isReactedByUser: users.contains(currentUserId), + userIds: users, + ); + }).toList(); +} + +/// Get the list of reactions that the user has reacted to +/// +/// [reactions] is a map [MessageReactions] where keys are emoji strings and values are lists of user IDs +/// [currentUserId] is used to determine if the current user has reacted +List getUserReactions( + Map>? reactions, + String currentUserId, +) { + if (reactions == null) { + return []; + } + return reactions.entries + .where((entry) => entry.value.contains(currentUserId)) + .map((entry) => entry.key) + .toList(); +} diff --git a/packages/flyer_chat_reactions/lib/src/utils/hover_float_effect.dart b/packages/flyer_chat_reactions/lib/src/utils/hover_float_effect.dart new file mode 100644 index 000000000..32d154e70 --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/utils/hover_float_effect.dart @@ -0,0 +1,79 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// A widget that applies a floating hover effect to its child. +/// +/// When the mouse hovers over this widget, it creates a subtle 3D-like effect +/// by translating and scaling the child based on mouse position. +/// +/// This effect is only applied on desktop platforms (Windows, macOS, Linux) and web. +/// On mobile platforms, the child widget is returned without any hover effects. +class HoverFloatEffect extends StatefulWidget { + /// The widget to apply the hover effect to. + final Widget child; + + /// The scale factor applied to the child when hovering. + /// A value of 1.0 means no scaling, 1.05 means 5% larger. + /// Defaults to 1.05 for a subtle zoom effect. + final double zoomScale; + + /// The translation distance in pixels for the hover effect. + /// Controls how far the widget moves from its center position. + /// Defaults to 20 pixels for a subtle floating effect. + final double translationDistance; + + const HoverFloatEffect({ + super.key, + required this.child, + this.zoomScale = 1.05, + this.translationDistance = 20, + }); + + @override + State createState() => _HoverFloatEffectState(); +} + +class _HoverFloatEffectState extends State { + Offset _offset = Offset.zero; + + /// Returns true if the current platform supports mouse hover effects. + bool get _supportsHover => + kIsWeb || + defaultTargetPlatform == TargetPlatform.windows || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux; + + @override + Widget build(BuildContext context) { + // On mobile platforms, just return the child without hover effects + if (!_supportsHover) { + return widget.child; + } + + final size = MediaQuery.of(context).size; + return MouseRegion( + onHover: (event) { + // Calculate normalized position (-0.5 to 0.5) and apply translation distance + final dx = + (event.position.dx / size.width - 0.5) * widget.translationDistance; + final dy = + (event.position.dy / size.height - 0.5) * + widget.translationDistance; + setState(() => _offset = Offset(dx, dy)); + }, + onExit: (_) => setState(() => _offset = Offset.zero), + child: TweenAnimationBuilder( + tween: Tween(begin: Offset.zero, end: _offset), + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + builder: (context, offset, child) { + return Transform.translate( + offset: offset, + child: Transform.scale(scale: widget.zoomScale, child: child), + ); + }, + child: widget.child, + ), + ); + } +} diff --git a/packages/flyer_chat_reactions/lib/src/utils/typedef.dart b/packages/flyer_chat_reactions/lib/src/utils/typedef.dart new file mode 100644 index 000000000..f940d9462 --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/utils/typedef.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +/// Callback signature for when a reaction is tapped. +typedef OnReactionTapCallback = void Function(String reaction); +typedef OnReactionLongPressCallback = void Function(String reaction); +typedef ReactionsDialogMoreReactionsWidgetBuilder = + Widget Function(BuildContext context); + +typedef ReactionsDialogBottomWidgetBuilder = + Widget Function(BuildContext context); diff --git a/packages/flyer_chat_reactions/lib/src/widgets/flyer_chat_reactions_row.dart b/packages/flyer_chat_reactions/lib/src/widgets/flyer_chat_reactions_row.dart new file mode 100644 index 000000000..8f2a793f9 --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/widgets/flyer_chat_reactions_row.dart @@ -0,0 +1,256 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:provider/provider.dart'; + +import '../helpers/chat_theme_extensions.dart'; +import '../models/reaction.dart'; +import '../utils/typedef.dart'; +import 'reaction_tile.dart'; + +/// A widget that displays a row of reaction tiles with emojis and counts. +/// +/// Handles layout and overflow of reactions, showing a surplus count when +/// there are more reactions than can fit in the available space. +class FlyerChatReactionsRow extends StatefulWidget { + /// The reactions to display, mapped by emoji. + final List reactions; + + /// Callback for when a reaction is tapped. + final OnReactionTapCallback? onReactionTap; + + /// Callback for when a reaction is long pressed. + final OnReactionLongPressCallback? onReactionLongPress; + + /// Callback when the surplus is tapped. + final VoidCallback? onSurplusReactionTap; + + /// Font size for the emoji in reaction tiles. + final TextStyle? emojiTextStyle; + + /// Text style for the count text in reaction tiles. + /// Note that we use a FittedBox so if the text is too long, it will be scaled down. + final TextStyle? countTextStyle; + + /// Text style for the surplus text in reaction tiles. + /// Note that we use a FittedBox so if the text is too long, it will be scaled down. + final TextStyle? surplusTextStyle; + + /// Space between reaction tiles. + /// Defaults to 2. + final double spacing; + + /// Inside padding for each [ReactionTile]. + /// Defaults to EdgeInsets.zero. + final EdgeInsets reactionTilePadding; + + /// Color of the border around reaction tiles. + /// If null, uses the default theme color. + final Color? borderColor; + + /// Background color for reaction tiles when not reacted by the user. + /// If null, uses the default theme color. + final Color? reactionBackgroundColor; + + /// Background color for reaction tiles when reacted by the user. + /// If null, uses the default theme color. + final Color? reactionReactedBackgroundColor; + + /// Alignment of the reactions row + final MainAxisAlignment alignment; + + /// Remove/Add on tap or not. + /// If true, the reaction will be removed locally when tapped. + final bool removeOrAddLocallyOnTap; + + /// Creates a widget that displays a row of reaction tiles. + const FlyerChatReactionsRow({ + super.key, + required this.reactions, + this.onReactionTap, + this.onReactionLongPress, + this.onSurplusReactionTap, + this.emojiTextStyle, + this.countTextStyle, + this.surplusTextStyle, + this.spacing = 2, + this.reactionTilePadding = const EdgeInsets.symmetric(horizontal: 4), + this.borderColor, + this.reactionBackgroundColor, + this.reactionReactedBackgroundColor, + this.alignment = MainAxisAlignment.start, + this.removeOrAddLocallyOnTap = false, + }); + + @override + State createState() => _FlyerChatReactionsRowState(); +} + +class _FlyerChatReactionsRowState extends State { + /// List of calculated sizes for each reaction tile. + final reactionsSizes = []; + + /// Calculates how many reactions can fit in the available width. + /// + /// Also updates [reactionsSizes] with the + /// calculated sizes for each visible reaction. + /// + /// Returns the number of reactions that can be displayed. + int calculateSizesAndMaxCapacity({ + required List reactions, + required double stackWidth, + required TextStyle emojiTextStyle, + required TextStyle countTextStyle, + required TextStyle extraTextStyle, + }) { + reactionsSizes.clear(); + double usedWidth = 0; + var visibleCount = 0; + final widgetCount = reactions.length; + + for (var i = 0; i < widgetCount; i++) { + final nextSize = ReactionTileSizeHelper.calculatePreferredSize( + emojiStyle: emojiTextStyle, + countTextStyle: countTextStyle, + extraTextStyle: extraTextStyle, + emoji: reactions[i].emoji, + countText: ReactionTileCountTextHelper.getCountString( + reactions[i].count, + ), + ); + final spaceNeeded = + usedWidth + nextSize.width + (visibleCount > 0 ? widget.spacing : 0); + if (spaceNeeded < stackWidth) { + usedWidth = spaceNeeded; + visibleCount++; + reactionsSizes.add(nextSize); + } else { + break; + } + } + return visibleCount; + } + + @override + Widget build(BuildContext context) { + final validReactions = widget.reactions.where((r) => r.count > 0).toList(); + if (validReactions.isEmpty) { + return const SizedBox.shrink(); + } + final theme = context.read(); + final emojiTextStyle = ReactionTileStyleResolver.resolveEmojiTextStyle( + provided: widget.emojiTextStyle, + theme: theme, + ); + final countTextStyle = ReactionTileStyleResolver.resolveCountTextStyle( + provided: widget.countTextStyle, + theme: theme, + ); + final extraTextStyle = ReactionTileStyleResolver.resolveExtraTextStyle( + provided: widget.surplusTextStyle, + theme: theme, + ); + + final reactedBackgroundColor = + widget.reactionReactedBackgroundColor ?? + theme.reactionReactedBackgroundColor; + final backgroundColor = + widget.reactionBackgroundColor ?? theme.reactionBackgroundColor; + + return LayoutBuilder( + builder: (context, BoxConstraints constraints) { + final isNotEnoughSpace = + constraints.maxWidth <= 0 || constraints.maxHeight <= 0; + if (isNotEnoughSpace) { + return const SizedBox.shrink(); + } + + final stackWidth = constraints.maxWidth; + var maxCapacity = calculateSizesAndMaxCapacity( + reactions: validReactions, + stackWidth: stackWidth, + emojiTextStyle: emojiTextStyle, + countTextStyle: countTextStyle, + extraTextStyle: extraTextStyle, + ); + var visibleItemsCount = reactionsSizes.length; + var hiddenCount = validReactions.length - maxCapacity; + final souldDisplaySurplus = hiddenCount > 0; + + Size? surplusWidgetSize; + if (souldDisplaySurplus) { + surplusWidgetSize = ReactionTileSizeHelper.calculatePreferredSize( + emojiStyle: emojiTextStyle, + countTextStyle: countTextStyle, + extraTextStyle: extraTextStyle, + extraText: '+$hiddenCount', + ); + maxCapacity = calculateSizesAndMaxCapacity( + reactions: validReactions, + stackWidth: stackWidth - surplusWidgetSize.width - widget.spacing, + emojiTextStyle: emojiTextStyle, + countTextStyle: countTextStyle, + extraTextStyle: extraTextStyle, + ); + + visibleItemsCount = reactionsSizes.length; + hiddenCount = validReactions.length - visibleItemsCount; + } + + final children = []; + + for (var i = 0; i < visibleItemsCount; i++) { + children.add( + ReactionTile( + key: ValueKey(validReactions[i].emoji), + width: reactionsSizes[i].width, + emoji: validReactions[i].emoji, + count: validReactions[i].count, + countTextStyle: countTextStyle, + emojiTextStyle: emojiTextStyle, + borderColor: theme.reactionBorderColor, + backgroundColor: backgroundColor, + reactedBackgroundColor: reactedBackgroundColor, + reactedByUser: validReactions[i].isReactedByUser, + onTap: () { + widget.onReactionTap?.call(validReactions[i].emoji); + }, + onLongPress: () { + widget.onReactionLongPress?.call(validReactions[i].emoji); + }, + removeOrAddLocallyOnTap: widget.removeOrAddLocallyOnTap, + ), + ); + } + + if (surplusWidgetSize != null) { + children.add( + ReactionTile( + key: const ValueKey('surplus'), + width: surplusWidgetSize.width, + extraText: '+$hiddenCount', + backgroundColor: backgroundColor, + reactedBackgroundColor: backgroundColor, + extraTextStyle: extraTextStyle, + borderColor: theme.reactionBorderColor, + onTap: () { + widget.onSurplusReactionTap?.call(); + }, + onLongPress: widget.onSurplusReactionTap, + ), + ); + } + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: widget.alignment, + mainAxisSize: MainAxisSize.max, + children: children, + ), + ], + ); + }, + ); + } +} diff --git a/packages/flyer_chat_reactions/lib/src/widgets/reaction_tile.dart b/packages/flyer_chat_reactions/lib/src/widgets/reaction_tile.dart new file mode 100644 index 000000000..dd91ceb02 --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/widgets/reaction_tile.dart @@ -0,0 +1,295 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:provider/provider.dart'; + +import '../helpers/chat_theme_extensions.dart'; + +/// A widget that displays a reaction with an emoji and optional count. +/// +/// Used to show individual reactions in the chat interface, supporting both +/// single emoji reactions, reactions with counts or a text (for surplus) + +class ReactionTile extends StatefulWidget { + /// The emoji to display as the reaction. + final String? emoji; + + /// The count of reactions for this emoji. + /// If null, the count is not shown. + /// If 0, the tile is not shown. + final int? count; + + /// The text to display, after the count text or alone. + /// Typically used for surplus reactions. + final String? extraText; + + /// Whether this reaction was added by the current user. + /// Affects the visual styling of the tile. + final bool reactedByUser; + + /// Callback triggered when the reaction tile is tapped. + /// Used to handle reaction selection/deselection. + final VoidCallback? onTap; + + /// Callback triggered when the reaction tile is long pressed. + /// Used to handle additional actions like showing a menu or details. + final VoidCallback? onLongPress; + + /// Background color for the reaction tile when not reacted by the user. + final Color? backgroundColor; + + /// Background color for the reaction tile when reacted by the user. + final Color? reactedBackgroundColor; + + /// Color of the border around the reaction tile. + final Color? borderColor; + + /// Text style for the count text and extra text. + final TextStyle? countTextStyle; + + /// Text style for the surplus text. + final TextStyle? extraTextStyle; + + /// Text style for the emoji. + final TextStyle? emojiTextStyle; + + /// Fixed width for the reaction tile. + /// If null, the tile will size itself based on its content and constraints. + final double? width; + + /// Fixed height for the reaction tile. + /// If null, uses the default height. + final double? height; + + /// Remove/Add on tap or not. + /// If true, the reaction will be removed locally when tapped. + final bool removeOrAddLocallyOnTap; + + /// Creates a reaction tile widget. + const ReactionTile({ + super.key, + this.emoji, + this.count, + this.extraText, + this.reactedByUser = false, + this.onTap, + this.onLongPress, + this.backgroundColor, + this.reactedBackgroundColor, + this.borderColor, + this.countTextStyle, + this.emojiTextStyle, + this.extraTextStyle, + this.width, + this.height, + this.removeOrAddLocallyOnTap = false, + }); + + @override + State createState() => _ReactionTileState(); +} + +class _ReactionTileState extends State { + late bool _isTapped; + late int? _count; + + @override + void initState() { + super.initState(); + _count = widget.count; + _isTapped = widget.reactedByUser; + } + + @override + void didUpdateWidget(ReactionTile oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.count != widget.count || + oldWidget.reactedByUser != widget.reactedByUser) { + setState(() { + _count = widget.count; + _isTapped = widget.reactedByUser; + }); + } + } + + @override + Widget build(BuildContext context) { + // Used to shrink on state update + if (_count == 0) { + return const SizedBox.shrink(); + } + final theme = context.read(); + final countString = + _count != null + ? ReactionTileCountTextHelper.getCountString(_count!) + : null; + return GestureDetector( + onTap: () { + if (widget.count != null && widget.removeOrAddLocallyOnTap) { + setState(() { + _count = _count! + (_isTapped ? -1 : 1); + _isTapped = !_isTapped; + }); + } + widget.onTap?.call(); + }, + onLongPress: widget.onLongPress, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: ReactionTileConstants.horizontalPadding, + ), + height: widget.height ?? ReactionTileConstants.height, + width: widget.width, + decoration: BoxDecoration( + color: + _isTapped + ? widget.reactedBackgroundColor + : widget.backgroundColor, + borderRadius: BorderRadius.circular(16), + border: + widget.borderColor != null + ? Border.all(color: widget.borderColor!, width: 1) + : null, + ), + child: ColoredBox( + color: Colors.transparent, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.emoji != null) + /// Emoji alignment issue https://github.com/flutter/flutter/issues/119623 + Padding( + padding: ReactionTileConstants.emojiAlignmentPadding, + child: Text( + widget.emoji!, + style: ReactionTileStyleResolver.resolveEmojiTextStyle( + provided: widget.emojiTextStyle, + theme: theme, + ), + ), + ), + if (widget.emoji != null && countString != null) + SizedBox(width: ReactionTileConstants.textElementsSpacing), + if (countString != null) + Text( + countString, + style: ReactionTileStyleResolver.resolveCountTextStyle( + provided: widget.countTextStyle, + theme: theme, + ), + ), + if ((countString != null || widget.emoji != null) && + widget.extraText != null) + SizedBox(width: ReactionTileConstants.textElementsSpacing), + if (widget.extraText != null) + Text( + widget.extraText!, + style: ReactionTileStyleResolver.resolveExtraTextStyle( + provided: widget.extraTextStyle, + theme: theme, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class ReactionTileConstants { + static const double textElementsSpacing = 2; + static const double minimumWidth = 40; + static const double horizontalPadding = 8; + static const double height = 24; + static const EdgeInsets emojiAlignmentPadding = EdgeInsets.fromLTRB( + 2.5, + 0, + 0, + 1.5, + ); +} + +class ReactionTileCountTextHelper { + static String? getCountString(int count) { + return count > 1 ? count.toString() : null; + } +} + +class ReactionTileSizeHelper { + /// Calculate the prefered size for the [ReactionTile] + /// + /// This is used to calculate the size of the [ReactionTile] + /// and the number of reactions that can fit in the available width. + /// + + static Size calculatePreferredSize({ + required TextStyle emojiStyle, + required TextStyle countTextStyle, + required TextStyle extraTextStyle, + String? emoji, + String? countText, + String? extraText, + }) { + final hasEmoji = emoji != null && emoji.isNotEmpty; + final hasText = countText != null && countText.isNotEmpty; + final hasExtraText = extraText != null && extraText.isNotEmpty; + if (!hasEmoji && !hasText && !hasExtraText) { + return Size.square(0); + } + var width = 0.0; + + if (hasEmoji) { + width += emojiStyle.fontSize ?? 12; + width += 2.5; // See emoji alignment + } + if (hasText) { + if (width > 0) { + width += ReactionTileConstants.textElementsSpacing; + } + width += countText.length * (countTextStyle.fontSize ?? 12); + } + if (hasExtraText) { + if (width > 0) { + width += ReactionTileConstants.textElementsSpacing; + } + width += extraText.length * (extraTextStyle.fontSize ?? 12); + } + width += ReactionTileConstants.horizontalPadding * 2; + + return Size( + width.clamp(ReactionTileConstants.minimumWidth, double.infinity), + ReactionTileConstants.height, + ); + } +} + +class ReactionTileStyleResolver { + static TextStyle resolveEmojiTextStyle({ + TextStyle? provided, + required ChatTheme theme, + }) { + if (provided != null) return provided; + return theme.reactionEmojiTextStyle; + } + + static TextStyle resolveCountTextStyle({ + TextStyle? provided, + required ChatTheme theme, + }) { + if (provided != null) return provided; + return theme.reactionCountTextStyle; + } + + static TextStyle resolveExtraTextStyle({ + TextStyle? provided, + required ChatTheme theme, + }) { + if (provided != null) return provided; + return theme.reactionSurplusTextStyle; + } +} diff --git a/packages/flyer_chat_reactions/lib/src/widgets/reactions_dialog.dart b/packages/flyer_chat_reactions/lib/src/widgets/reactions_dialog.dart new file mode 100644 index 000000000..42635baa4 --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/widgets/reactions_dialog.dart @@ -0,0 +1,289 @@ +import 'dart:ui'; +import 'package:animate_do/animate_do.dart' show FadeInLeft, Pulse; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart' + show ChatProviders, buildMessageContent; +import 'package:provider/provider.dart'; + +import '../models/default_data.dart'; +import '../utils/hover_float_effect.dart'; +import '../utils/typedef.dart'; + +//// Theme values for [ReactionsDialogWidget]. +typedef _LocalTheme = + ({ + Color onSurface, + Color surfaceContainer, + Color primary, + BorderRadiusGeometry shape, + }); + +class ReactionsDialogWidget extends StatefulWidget { + const ReactionsDialogWidget({ + super.key, + required this.messageWidget, + required this.onReactionTap, + this.moreReactionsWidgetBuilder, + this.onMoreReactionsTap, + this.reactions, + this.userReactions, + this.horizontalAlignment = CrossAxisAlignment.end, + this.reactionsPickerBackgroundColor, + this.reactionsPickerReactedBackgroundColor, + this.reactionTapAnimationDuration, + this.reactionPickerFadeLeftAnimationDuration, + this.activateHoverFloatEffect = true, + this.bottomWidgetBuilder, + }); + + /// The message widget to be displayed in the dialog + final Widget messageWidget; + + /// The callback function to be called when a reaction is tapped + final OnReactionTapCallback onReactionTap; + + /// More Reactions Widget builder + final ReactionsDialogMoreReactionsWidgetBuilder? moreReactionsWidgetBuilder; + + /// The callback function to be called when the "more" reactions widget is tapped + /// If not provided the widget will not be displayed + final VoidCallback? onMoreReactionsTap; + + /// The list of menu items to be displayed in the context menu + final ReactionsDialogBottomWidgetBuilder? bottomWidgetBuilder; + + /// The list of default reactions to be displayed + final List? reactions; + + /// The list of user reactions to be displayed + /// This allow user to remove them from here + final List? userReactions; + + /// The horizontal alignment of the widget + final CrossAxisAlignment horizontalAlignment; + + /// The background color for reactions picker + final Color? reactionsPickerBackgroundColor; + + /// The color for the reactions reacted by the user + final Color? reactionsPickerReactedBackgroundColor; + + /// Animation duration when a reaction is selected + final Duration? reactionTapAnimationDuration; + + /// Animation duration to display the reactions row + final Duration? reactionPickerFadeLeftAnimationDuration; + + /// Whether to activate the hover float effect + final bool activateHoverFloatEffect; + + @override + State createState() => _ReactionsDialogWidgetState(); +} + +class _ReactionsDialogWidgetState extends State { + bool reactionClicked = false; + int? clickedReactionIndex; + int? clickedContextMenuIndex; + + @override + Widget build(BuildContext context) { + final theme = context.select( + (ChatTheme t) => ( + onSurface: t.colors.onSurface, + surfaceContainer: t.colors.surfaceContainerHigh, + primary: t.colors.primary, + shape: t.shape, + ), + ); + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Padding( + padding: const EdgeInsets.only(right: 32.0, left: 32.0), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: widget.horizontalAlignment, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildReactionsPicker(context, theme), + const SizedBox(height: 10), + widget.activateHoverFloatEffect + ? HoverFloatEffect(child: widget.messageWidget) + : widget.messageWidget, + if (widget.bottomWidgetBuilder != null) ...[ + const SizedBox(height: 10), + widget.bottomWidgetBuilder!(context), + ], + ], + ), + ), + ); + } + + Widget buildReactionsPicker(BuildContext context, _LocalTheme theme) { + // Merge default reactions with user reactions, removing duplicates + final allReactions = + { + ...(widget.reactions ?? DefaultData.reactions), + ...(widget.userReactions ?? const []), + }.toList(); + + final reactionTapAnimationDuration = + widget.reactionTapAnimationDuration ?? + const Duration(milliseconds: 200); + return Material( + color: Colors.transparent, + child: Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: + widget.reactionsPickerBackgroundColor ?? theme.surfaceContainer, + borderRadius: theme.shape, + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < allReactions.length; i++) + FadeInLeft( + from: 0 + (i * 20).toDouble(), + duration: + widget.reactionPickerFadeLeftAnimationDuration ?? + const Duration(milliseconds: 200), + delay: Duration.zero, + child: InkWell( + child: Container( + margin: const EdgeInsets.only(right: 2), + padding: const EdgeInsets.fromLTRB(4.0, 2.0, 4.0, 2), + decoration: BoxDecoration( + color: + (widget.userReactions ?? const []).contains( + allReactions[i], + ) + ? widget.reactionsPickerReactedBackgroundColor ?? + theme.onSurface.withValues(alpha: 0.2) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Pulse( + infinite: false, + duration: reactionTapAnimationDuration, + animate: reactionClicked && clickedReactionIndex == i, + child: Text( + allReactions[i], + style: TextStyle(fontSize: 22), + ), + ), + ), + onTap: () { + setState(() { + reactionClicked = true; + clickedReactionIndex = i; + }); + Future.delayed(reactionTapAnimationDuration).whenComplete( + () { + if (context.mounted) { + Navigator.of(context).pop(); + } + widget.onReactionTap(allReactions[i]); + }, + ); + }, + ), + ), + if (widget.onMoreReactionsTap != null) + FadeInLeft( + from: 0 + (allReactions.length * 20).toDouble(), + duration: + widget.reactionPickerFadeLeftAnimationDuration ?? + const Duration(milliseconds: 200), + delay: Duration.zero, + child: InkWell( + onTap: () { + if (context.mounted) { + Navigator.of(context).pop(); + } + widget.onMoreReactionsTap?.call(); + }, + child: + widget.moreReactionsWidgetBuilder?.call(context) ?? + Padding( + padding: const EdgeInsets.fromLTRB(4.0, 2.0, 4.0, 2), + child: Icon( + Icons.more_horiz_rounded, + color: theme.onSurface, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +/// Method to display the reactions dialog for a message +/// Refer to [ReactionsDialogWidget] for the available parameters +/// +void showReactionsDialog( + BuildContext context, + Message message, { + required bool isSentByMe, + required OnReactionTapCallback onReactionTap, + VoidCallback? onMoreReactionsTap, + List? reactions, + List? userReactions, + CrossAxisAlignment? horizontalAlignment, + Color? reactionsPickerBackgroundColor, + Color? reactionsPickerReactedBackgroundColor, + Duration? reactionTapAnimationDuration, + Duration? reactionPickerFadeLeftAnimationDuration, + ReactionsDialogMoreReactionsWidgetBuilder? moreReactionsWidgetBuilder, + ReactionsDialogBottomWidgetBuilder? bottomWidgetBuilder, + bool activateHoverFloatEffect = true, +}) { + final providers = ChatProviders.from(context); + + final widget = buildMessageContent( + context, + context.read(), + message, + 0, + isSentByMe: isSentByMe, + ); + + showDialog( + context: context, + useSafeArea: true, + useRootNavigator: false, + builder: + (context) => MultiProvider( + providers: providers, + child: ReactionsDialogWidget( + messageWidget: widget, + horizontalAlignment: + horizontalAlignment ?? + (isSentByMe + ? CrossAxisAlignment.end + : CrossAxisAlignment.start), + onReactionTap: onReactionTap, + onMoreReactionsTap: onMoreReactionsTap, + bottomWidgetBuilder: bottomWidgetBuilder, + reactions: reactions, + userReactions: userReactions, + reactionsPickerBackgroundColor: reactionsPickerBackgroundColor, + reactionsPickerReactedBackgroundColor: + reactionsPickerReactedBackgroundColor, + reactionTapAnimationDuration: reactionTapAnimationDuration, + reactionPickerFadeLeftAnimationDuration: + reactionPickerFadeLeftAnimationDuration, + moreReactionsWidgetBuilder: moreReactionsWidgetBuilder, + activateHoverFloatEffect: activateHoverFloatEffect, + ), + ), + ); +} diff --git a/packages/flyer_chat_reactions/lib/src/widgets/reactions_list.dart b/packages/flyer_chat_reactions/lib/src/widgets/reactions_list.dart new file mode 100644 index 000000000..7ce609593 --- /dev/null +++ b/packages/flyer_chat_reactions/lib/src/widgets/reactions_list.dart @@ -0,0 +1,340 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'package:provider/provider.dart'; + +import '../models/reaction.dart'; + +/// Theme values for [ReactionsList]. +typedef _LocalTheme = + ({ + Color backgroundColor, + Color selectedFilterChipColor, + Color unselectedFilterChipColor, + TextStyle filterChipTextStyle, + TextStyle listEmojiTextStyle, + TextStyle listCountTextStyle, + TextStyle listUsernamesTextStyle, + }); + +/// A widget that displays a list of users and their reactions in a bottom sheet. +/// +/// Used to show who reacted with which emoji, typically shown when long-pressing +/// a reaction tile. +class ReactionsList extends StatefulWidget { + /// The list of reactions to display. + final List reactions; + + /// The config for the reactions list. + final ReactionListStyleConfig styleConfig; + + /// Creates a widget that displays a list of users and their reactions. + const ReactionsList({ + super.key, + required this.reactions, + this.styleConfig = const ReactionListStyleConfig(), + }); + + @override + State createState() => _ReactionsListState(); +} + +class _ReactionsListState extends State { + String? selectedEmoji; + late Future> _userNamesFuture; + + @override + void initState() { + super.initState(); + _initUserNamesFuture(); + } + + @override + void didUpdateWidget(covariant ReactionsList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.reactions != widget.reactions) { + _initUserNamesFuture(); + } + } + + void _initUserNamesFuture() { + final resolveUser = context.read(); + final userCache = context.read(); + _userNamesFuture = _resolveUserNames(context, resolveUser, userCache); + } + + Future> _resolveUserNames( + BuildContext context, + ResolveUserCallback resolveUser, + UserCache userCache, + ) async { + final userMap = {}; + for (final reaction in widget.reactions) { + for (final userId in reaction.userIds) { + if (!userMap.containsKey(userId)) { + final resolvedUser = await userCache.getOrResolve( + userId, + resolveUser, + ); + userMap[userId] = resolvedUser?.name ?? userId; + } + } + } + return userMap; + } + + @override + Widget build(BuildContext context) { + final theme = context.select( + (ChatTheme t) => ( + backgroundColor: + widget.styleConfig.backgroundColor ?? t.colors.surfaceContainerHigh, + selectedFilterChipColor: + widget.styleConfig.filterChipsSelectedColor ?? + t.colors.onPrimary.withValues(alpha: 0.2), + unselectedFilterChipColor: + widget.styleConfig.filterChipsUnselectedColor ?? + t.colors.onSurface.withValues(alpha: 0.2), + filterChipTextStyle: + widget.styleConfig.filterChipsTextStyles ?? t.typography.bodyMedium, + listEmojiTextStyle: + widget.styleConfig.listEmojiTextStyle ?? t.typography.bodyMedium, + listCountTextStyle: + widget.styleConfig.listCountTextStyle ?? t.typography.bodyMedium, + listUsernamesTextStyle: + widget.styleConfig.listUsernamesTextStyle ?? + t.typography.bodyMedium, + ), + ); + final validReactions = widget.reactions.where((r) => r.count > 0).toList(); + + final filteredReactions = + selectedEmoji == null + ? validReactions + : validReactions.where((r) => r.emoji == selectedEmoji).toList(); + + final totalReactionCount = widget.reactions.fold( + 0, + (sum, r) => sum + r.count, + ); + + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: theme.backgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Filter chips + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + _buildChip( + label: Text( + '${widget.styleConfig.allFilterChipLabel} • $totalReactionCount', + ), + theme: theme, + selected: selectedEmoji == null, + onSelected: (selected) { + setState(() { + selectedEmoji = null; + }); + }, + ), + const SizedBox(width: 8), + ...validReactions.map( + (reaction) => Padding( + padding: const EdgeInsets.only(right: 8), + child: _buildChip( + label: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(reaction.emoji), + const SizedBox(width: 4), + Text('${reaction.count}'), + ], + ), + theme: theme, + selected: selectedEmoji == reaction.emoji, + onSelected: (selected) { + setState(() { + selectedEmoji = selected ? reaction.emoji : null; + }); + }, + ), + ), + ), + ], + ), + ), + // Reactions list + Flexible( + child: FutureBuilder>( + future: _userNamesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ); + } + + final userMap = snapshot.data ?? {}; + + return SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + child: ListView.builder( + shrinkWrap: true, + itemCount: filteredReactions.length, + itemBuilder: (context, index) { + final reaction = filteredReactions[index]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + children: [ + Text( + reaction.emoji, + style: widget.styleConfig.listEmojiTextStyle, + ), + const SizedBox(width: 8), + Text( + '${reaction.count}', + style: widget.styleConfig.listCountTextStyle, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 4, + ), + child: Text( + reaction.userIds + .map((userId) => userMap[userId] ?? userId) + .join(', '), + style: theme.listUsernamesTextStyle, + ), + ), + if (index < filteredReactions.length - 1) + Divider( + height: 1, + color: theme.unselectedFilterChipColor.withValues( + alpha: 0.2, + ), + ), + ], + ); + }, + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildChip({ + required Widget label, + required _LocalTheme theme, + void Function(bool)? onSelected, + required bool selected, + }) { + return FilterChip( + label: label, + showCheckmark: false, + selectedColor: theme.selectedFilterChipColor, + backgroundColor: theme.backgroundColor, + shadowColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + side: BorderSide.none, + onSelected: onSelected, + selected: selected, + ); + } +} + +/// Shows a bottom sheet with the list of reactions. +/// Must be called with a context from the Chat for providers to be available. +Future showReactionsList({ + required BuildContext context, + required List reactions, + ReactionListStyleConfig? styleConfig, +}) { + final providers = ChatProviders.from(context); + return showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: + (context) => MultiProvider( + providers: providers, + child: ReactionsList( + reactions: reactions, + styleConfig: styleConfig ?? const ReactionListStyleConfig(), + ), + ), + ); +} + +class ReactionListStyleConfig { + /// Label for All filter chips. Default is 'All' + final String allFilterChipLabel; + + /// The style for the user ID text. + final TextStyle? listUsernamesTextStyle; + + /// The style for the filter chips text. + final TextStyle? filterChipsTextStyles; + + /// The background color for selected filter chips. + final Color? filterChipsSelectedColor; + + /// The background color for unselected filter chips. + final Color? filterChipsUnselectedColor; + + /// The style for the header title text. + final TextStyle? headerTitleTextStyle; + + /// The style for the header count text. + final TextStyle? headerCountTextStyle; + + /// Font size for the emoji in reaction tiles. + final TextStyle? listEmojiTextStyle; + + /// The style for the reaction count text. + final TextStyle? listCountTextStyle; + + /// Background color for the bottom sheet. + final Color? backgroundColor; + + const ReactionListStyleConfig({ + this.allFilterChipLabel = 'All', + this.listUsernamesTextStyle, + this.filterChipsTextStyles, + this.filterChipsSelectedColor, + this.filterChipsUnselectedColor, + this.headerTitleTextStyle, + this.headerCountTextStyle, + this.listEmojiTextStyle, + this.listCountTextStyle, + this.backgroundColor, + }); +} diff --git a/packages/flyer_chat_reactions/pubspec.yaml b/packages/flyer_chat_reactions/pubspec.yaml new file mode 100644 index 000000000..9b9b6319a --- /dev/null +++ b/packages/flyer_chat_reactions/pubspec.yaml @@ -0,0 +1,23 @@ +name: flyer_chat_reactions +version: 0.0.12 +description: > + Reactions package for Flutter chat apps, complementing flutter_chat_ui. #chat #ui +homepage: https://flyer.chat +repository: https://github.com/flyerhq/flutter_chat_ui + +environment: + sdk: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" + +dependencies: + animate_do: ^4.2.0 + flutter: + sdk: flutter + flutter_chat_core: ^2.7.0 + flutter_chat_ui: ^2.7.0 + provider: ^6.1.5 + +dev_dependencies: + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter