diff --git a/lib/dash_chat_2.dart b/lib/dash_chat_2.dart index b065fa3..9b106ff 100644 --- a/lib/dash_chat_2.dart +++ b/lib/dash_chat_2.dart @@ -21,6 +21,7 @@ part 'src/models/cursor_style.dart'; part 'src/models/input_options.dart'; part 'src/models/mention.dart'; part 'src/models/message_list_options.dart'; +part 'src/models/custom_separators.dart'; part 'src/models/message_options.dart'; part 'src/models/quick_reply.dart'; part 'src/models/quick_reply_options.dart'; diff --git a/lib/src/models/chat_message.dart b/lib/src/models/chat_message.dart index e03c4c3..108aa07 100644 --- a/lib/src/models/chat_message.dart +++ b/lib/src/models/chat_message.dart @@ -5,6 +5,7 @@ class ChatMessage { ChatMessage({ required this.user, required this.createdAt, + this.id, this.text = '', this.medias, this.quickReplies, @@ -17,6 +18,7 @@ class ChatMessage { /// Create a ChatMessage instance from json data factory ChatMessage.fromJson(Map jsonData) { return ChatMessage( + id: (jsonData['messageId'] ?? jsonData['id']).toString(), user: ChatUser.fromJson(jsonData['user'] as Map), createdAt: DateTime.parse(jsonData['createdAt'].toString()).toLocal(), text: jsonData['text']?.toString() ?? '', @@ -46,6 +48,10 @@ class ChatMessage { ); } + /// Unique id for a ChatMessage (optional because during sending a message + /// we might not have an id, maybe its generated on the server side) + String? id; + /// Text of the message (optional because you can also just send a media) String text; diff --git a/lib/src/models/custom_separators.dart b/lib/src/models/custom_separators.dart new file mode 100644 index 0000000..473ddab --- /dev/null +++ b/lib/src/models/custom_separators.dart @@ -0,0 +1,11 @@ +part of dash_chat_2; +// ignore_for_file: non_constant_identifier_names + +class CustomSeparator { + CustomSeparator({ + required this.shouldShowSeparator, + required this.separator, + }); + bool Function(ChatMessage currentMessage) shouldShowSeparator; + Widget separator; +} diff --git a/lib/src/models/message_list_options.dart b/lib/src/models/message_list_options.dart index fb9e72b..5016200 100644 --- a/lib/src/models/message_list_options.dart +++ b/lib/src/models/message_list_options.dart @@ -6,6 +6,7 @@ class MessageListOptions { this.showDateSeparator = true, this.dateSeparatorFormat, this.dateSeparatorBuilder, + this.customSeparators, this.separatorFrequency = SeparatorFrequency.days, this.scrollController, this.chatFooterBuilder, @@ -27,6 +28,8 @@ class MessageListOptions { /// You can use DefaultDateSeparator to only override some variables final Widget Function(DateTime date)? dateSeparatorBuilder; + final List? customSeparators; + /// The frequency of the separator final SeparatorFrequency separatorFrequency; diff --git a/lib/src/models/message_options.dart b/lib/src/models/message_options.dart index fe19a5a..d8f2bd9 100644 --- a/lib/src/models/message_options.dart +++ b/lib/src/models/message_options.dart @@ -11,6 +11,7 @@ class MessageOptions { this.onPressAvatar, this.onLongPressAvatar, this.onLongPressMessage, + this.onTapDownMessage, this.onPressMessage, this.onPressMention, Color? currentUserContainerColor, @@ -28,11 +29,13 @@ class MessageOptions { this.textBeforeMedia = true, this.onTapMedia, this.showTime = false, + this.showStatus = false, this.timeFormat, this.messageTimeBuilder, this.messageMediaBuilder, this.borderRadius = 18.0, Color? currentUserTimeTextColor, + Color? currentUserReadStatusIconColor, this.marginDifferentAuthor = const EdgeInsets.only(top: 15), this.marginSameAuthor = const EdgeInsets.only(top: 2), this.spaceWhenAvatarIsHidden = 10.0, @@ -42,6 +45,7 @@ class MessageOptions { }) : _currentUserContainerColor = currentUserContainerColor, _currentUserTextColor = currentUserTextColor, _currentUserTimeTextColor = currentUserTimeTextColor, + _currentUserReadStatusIconColor = currentUserReadStatusIconColor, _timeTextColor = timeTextColor; /// Format of the time if [showTime] is true @@ -51,6 +55,9 @@ class MessageOptions { /// If you want to show the time under the text of each message final bool showTime; + /// If you want to show the status under the text of each message + final bool showStatus; + /// If you want to show the avatar of the current user final bool showCurrentUserAvatar; @@ -80,6 +87,9 @@ class MessageOptions { /// Function to call when the user long press on a message final Function(ChatMessage)? onLongPressMessage; + /// Function to call when the user taps on a message + final Function(TapDownDetails, ChatMessage)? onTapDownMessage; + /// Function to call when the user press on a message final Function(ChatMessage)? onPressMessage; @@ -103,6 +113,7 @@ class MessageOptions { return _currentUserTextColor ?? Theme.of(context).colorScheme.onPrimary; } + /// Used to calculate [currentUserTextColor] final Color? _currentUserTextColor; @@ -116,6 +127,18 @@ class MessageOptions { /// Used to calculate [currentUserTimeTextColor] final Color? _currentUserTimeTextColor; + + /// Color of current user time text in chat bubbles + /// + /// Default to: `currentUserTextColor` + Color currentUserReadStatusIconColor(BuildContext context) { + return _currentUserReadStatusIconColor ?? currentUserTextColor(context); + } + + /// Used to calculate [currentUserReadStatusIconColor] + final Color? _currentUserReadStatusIconColor; + + /// Color of the other users chat bubbles /// /// Default to: `Colors.grey.shade100` diff --git a/lib/src/widgets/message_list/message_list.dart b/lib/src/widgets/message_list/message_list.dart index 4bf2897..f1e8689 100644 --- a/lib/src/widgets/message_list/message_list.dart +++ b/lib/src/widgets/message_list/message_list.dart @@ -96,6 +96,18 @@ class MessageListState extends State { date: message.createdAt, messageListOptions: widget.messageListOptions, ), + if(widget.messageListOptions.customSeparators != null) + ...widget.messageListOptions.customSeparators! + .map( + (CustomSeparator cs) { + return Visibility( + visible: cs.shouldShowSeparator(message), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 5.0), + child: cs.separator + ), + ); + }).toList(), if (widget.messageOptions.messageRowBuilder != null) ...[ widget.messageOptions.messageRowBuilder!( @@ -158,13 +170,13 @@ class MessageListState extends State { if (!widget.scrollToBottomOptions.disabled && scrollToBottomIsVisible) widget.scrollToBottomOptions.scrollToBottomBuilder != null ? widget.scrollToBottomOptions - .scrollToBottomBuilder!(scrollController) + .scrollToBottomBuilder!(scrollController) : DefaultScrollToBottom( - scrollController: scrollController, - readOnly: widget.readOnly, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - textColor: Theme.of(context).primaryColor, - ), + scrollController: scrollController, + readOnly: widget.readOnly, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + textColor: Theme.of(context).primaryColor, + ), ], ), ); diff --git a/lib/src/widgets/message_row/default_message_text.dart b/lib/src/widgets/message_row/default_message_text.dart index 53be84d..bebd5ab 100644 --- a/lib/src/widgets/message_row/default_message_text.dart +++ b/lib/src/widgets/message_row/default_message_text.dart @@ -20,29 +20,59 @@ class DefaultMessageText extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: - isOwnMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, + return Stack( children: [ Wrap( - children: getMessage(context), + verticalDirection: VerticalDirection.down, + alignment: WrapAlignment.end, + children: [ + ...getMessage(context), + // for reserved text + if (isOwnMessage) const Text(' \u202F') + // here the icon for read receipt is generally not shown so we need lesser space + else const Text(' \u202F'), + ], ), - if (messageOptions.showTime) - messageOptions.messageTimeBuilder != null - ? messageOptions.messageTimeBuilder!(message, isOwnMessage) - : Padding( - padding: messageOptions.timePadding, - child: Text( - (messageOptions.timeFormat ?? intl.DateFormat('HH:mm')) - .format(message.createdAt), - style: TextStyle( - color: isOwnMessage - ? messageOptions.currentUserTimeTextColor(context) - : messageOptions.timeTextColor(), - fontSize: messageOptions.timeFontSize, + Positioned( + bottom: 0, + right: 0, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (messageOptions.showTime) + messageOptions.messageTimeBuilder != null + ? messageOptions.messageTimeBuilder!(message, isOwnMessage) + : Padding( + padding: messageOptions.timePadding, + child: Text( + (messageOptions.timeFormat ?? intl.DateFormat('HH:mm')) + .format(message.createdAt), + style: TextStyle( + color: isOwnMessage + ? messageOptions.currentUserTimeTextColor(context) + : messageOptions.timeTextColor(), + fontSize: messageOptions.timeFontSize, + ), ), ), - ), + Visibility( + visible: isOwnMessage, + child: Container( + margin: const EdgeInsets.only(left: 5), + child: messageOptions.showStatus ? + message.status == MessageStatus.read ? Icon( + Icons.done_all, color: messageOptions.currentUserReadStatusIconColor(context), size: 15, + ) : + message.status == MessageStatus.received ? Icon( + Icons.check, color: messageOptions.currentUserReadStatusIconColor(context), size: 15 + ) : + Container() : Container() + ), + ) + ] + ), + ), ], ); } @@ -68,7 +98,7 @@ class DefaultMessageText extends StatelessWidget { .forEach((String? part) { if (mentionRegex.hasMatch(part!)) { Mention mention = message.mentions!.firstWhere( - (Mention m) => m.title == part, + (Mention m) => m.title == part, ); res.add(getMention(context, mention)); } else { diff --git a/lib/src/widgets/message_row/message_row.dart b/lib/src/widgets/message_row/message_row.dart index fc0cc93..51de63f 100644 --- a/lib/src/widgets/message_row/message_row.dart +++ b/lib/src/widgets/message_row/message_row.dart @@ -84,6 +84,9 @@ class MessageRow extends StatelessWidget { if (!messageOptions.showOtherUsersAvatar) SizedBox(width: messageOptions.spaceWhenAvatarIsHidden), GestureDetector( + onTapDown: messageOptions.onTapDownMessage != null + ? (TapDownDetails td) => messageOptions.onTapDownMessage!(td, message) + : null, onLongPress: messageOptions.onLongPressMessage != null ? () => messageOptions.onLongPressMessage!(message) : null,