From 36a19ead8e4affb08aad7007b524ecd8669cc49e Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 12 Jan 2025 20:57:39 +0900 Subject: [PATCH 01/26] =?UTF-8?q?test=20=EC=BD=94=EB=93=9C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/models/chat_bubble.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/models/chat_bubble.dart b/lib/src/models/chat_bubble.dart index dbc9c9f4..c1b05680 100644 --- a/lib/src/models/chat_bubble.dart +++ b/lib/src/models/chat_bubble.dart @@ -52,6 +52,8 @@ class ChatBubble { /// time only final Function(Message message)? onMessageRead; + final Widget? messageTimeAndUnreadCount; + const ChatBubble({ this.color, this.borderRadius, @@ -62,5 +64,6 @@ class ChatBubble { this.senderNameTextStyle, this.receiptsWidgetConfig, this.onMessageRead, + this.messageTimeAndUnreadCount, }); } From 3bf24817418642e7e96fce7b60bb90224e59a2ad Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 12 Jan 2025 21:01:52 +0900 Subject: [PATCH 02/26] =?UTF-8?q?Revert=20"test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 36a19ead8e4affb08aad7007b524ecd8669cc49e. --- lib/src/models/chat_bubble.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/models/chat_bubble.dart b/lib/src/models/chat_bubble.dart index c1b05680..dbc9c9f4 100644 --- a/lib/src/models/chat_bubble.dart +++ b/lib/src/models/chat_bubble.dart @@ -52,8 +52,6 @@ class ChatBubble { /// time only final Function(Message message)? onMessageRead; - final Widget? messageTimeAndUnreadCount; - const ChatBubble({ this.color, this.borderRadius, @@ -64,6 +62,5 @@ class ChatBubble { this.senderNameTextStyle, this.receiptsWidgetConfig, this.onMessageRead, - this.messageTimeAndUnreadCount, }); } From 8ac1fad9ae215279217244e4b899e8d7600b82ff Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 12 Jan 2025 21:04:28 +0900 Subject: [PATCH 03/26] =?UTF-8?q?test=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/models/chat_bubble.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/models/chat_bubble.dart b/lib/src/models/chat_bubble.dart index dbc9c9f4..c1b05680 100644 --- a/lib/src/models/chat_bubble.dart +++ b/lib/src/models/chat_bubble.dart @@ -52,6 +52,8 @@ class ChatBubble { /// time only final Function(Message message)? onMessageRead; + final Widget? messageTimeAndUnreadCount; + const ChatBubble({ this.color, this.borderRadius, @@ -62,5 +64,6 @@ class ChatBubble { this.senderNameTextStyle, this.receiptsWidgetConfig, this.onMessageRead, + this.messageTimeAndUnreadCount, }); } From d1279e70c997cb85668d6b0bab89ff3aa24dc828 Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 12 Jan 2025 21:41:02 +0900 Subject: [PATCH 04/26] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/models/chat_bubble.dart | 3 --- lib/src/widgets/chat_bubble_widget.dart | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/models/chat_bubble.dart b/lib/src/models/chat_bubble.dart index c1b05680..dbc9c9f4 100644 --- a/lib/src/models/chat_bubble.dart +++ b/lib/src/models/chat_bubble.dart @@ -52,8 +52,6 @@ class ChatBubble { /// time only final Function(Message message)? onMessageRead; - final Widget? messageTimeAndUnreadCount; - const ChatBubble({ this.color, this.borderRadius, @@ -64,6 +62,5 @@ class ChatBubble { this.senderNameTextStyle, this.receiptsWidgetConfig, this.onMessageRead, - this.messageTimeAndUnreadCount, }); } diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index 4c748f96..b22cedce 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -130,10 +130,11 @@ class _ChatBubbleWidgetState extends State { if (!isMessageBySender && (featureActiveConfig?.enableOtherUserProfileAvatar ?? true)) profileCircle(messagedUser), + if (isMessageBySender) ...[getReceipt()], Expanded( child: _messagesWidgetColumn(messagedUser), ), - if (isMessageBySender) ...[getReceipt()], + if (!isMessageBySender) ...[getReceipt()], if (isMessageBySender && (featureActiveConfig?.enableCurrentUserProfileAvatar ?? true)) profileCircle(messagedUser), From 7e84958c9dfb7e3e9efe71c383aea00d81f2e7ce Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 12 Jan 2025 21:56:31 +0900 Subject: [PATCH 05/26] =?UTF-8?q?Revert=20"=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=98=EC=98=81"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d1279e70c997cb85668d6b0bab89ff3aa24dc828. --- lib/src/models/chat_bubble.dart | 3 +++ lib/src/widgets/chat_bubble_widget.dart | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/models/chat_bubble.dart b/lib/src/models/chat_bubble.dart index dbc9c9f4..c1b05680 100644 --- a/lib/src/models/chat_bubble.dart +++ b/lib/src/models/chat_bubble.dart @@ -52,6 +52,8 @@ class ChatBubble { /// time only final Function(Message message)? onMessageRead; + final Widget? messageTimeAndUnreadCount; + const ChatBubble({ this.color, this.borderRadius, @@ -62,5 +64,6 @@ class ChatBubble { this.senderNameTextStyle, this.receiptsWidgetConfig, this.onMessageRead, + this.messageTimeAndUnreadCount, }); } diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index b22cedce..4c748f96 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -130,11 +130,10 @@ class _ChatBubbleWidgetState extends State { if (!isMessageBySender && (featureActiveConfig?.enableOtherUserProfileAvatar ?? true)) profileCircle(messagedUser), - if (isMessageBySender) ...[getReceipt()], Expanded( child: _messagesWidgetColumn(messagedUser), ), - if (!isMessageBySender) ...[getReceipt()], + if (isMessageBySender) ...[getReceipt()], if (isMessageBySender && (featureActiveConfig?.enableCurrentUserProfileAvatar ?? true)) profileCircle(messagedUser), From fd54230cec7071aa087975060f0e3e57148e34fa Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 12 Jan 2025 22:41:05 +0900 Subject: [PATCH 06/26] =?UTF-8?q?chatbubble=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/chat_bubble_widget.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index 4c748f96..aaa54665 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -121,8 +121,9 @@ class _ChatBubbleWidgetState extends State { return Container( padding: chatBubbleConfig?.padding ?? const EdgeInsets.only(left: 5.0), margin: chatBubbleConfig?.margin ?? const EdgeInsets.only(bottom: 10), + width: double.infinity, + alignment: isMessageBySender ? Alignment.centerRight : Alignment.centerLeft, child: Row( - mainAxisSize: MainAxisSize.min, mainAxisAlignment: isMessageBySender ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, @@ -130,10 +131,9 @@ class _ChatBubbleWidgetState extends State { if (!isMessageBySender && (featureActiveConfig?.enableOtherUserProfileAvatar ?? true)) profileCircle(messagedUser), - Expanded( - child: _messagesWidgetColumn(messagedUser), - ), - if (isMessageBySender) ...[getReceipt()], + if (isMessageBySender) getReceipt(), + _messagesWidgetColumn(messagedUser), + if (!isMessageBySender) getReceipt(), if (isMessageBySender && (featureActiveConfig?.enableCurrentUserProfileAvatar ?? true)) profileCircle(messagedUser), From 645f185aa3c98e700d648c14606a160d13684c45 Mon Sep 17 00:00:00 2001 From: bmlee Date: Fri, 31 Jan 2025 12:43:00 +0900 Subject: [PATCH 07/26] =?UTF-8?q?chatview-ui=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/controller/chat_controller.dart | 7 + lib/src/models/chat_bubble.dart | 3 - .../profile_circle_configuration.dart | 4 + .../config_models/receipts_widget_config.dart | 2 +- lib/src/models/data_models/chat_user.dart | 32 ++ lib/src/models/data_models/message.dart | 8 + lib/src/widgets/chat_bubble_widget.dart | 139 ++++---- lib/src/widgets/chatui_textfield.dart | 139 ++++---- lib/src/widgets/link_preview.dart | 64 ++-- lib/src/widgets/profile_circle.dart | 29 +- lib/src/widgets/send_message_widget.dart | 300 +++++++++--------- lib/src/widgets/text_message_view.dart | 64 ++-- 12 files changed, 434 insertions(+), 357 deletions(-) diff --git a/lib/src/controller/chat_controller.dart b/lib/src/controller/chat_controller.dart index 6583577a..1f40e2e2 100644 --- a/lib/src/controller/chat_controller.dart +++ b/lib/src/controller/chat_controller.dart @@ -110,6 +110,13 @@ class ChatController { _replySuggestion.value = []; } + void syncMessageList(List messageList) { + initialMessageList = messageList as List; + if (!messageStreamController.isClosed) { + messageStreamController.sink.add(initialMessageList); + } + } + /// Function for setting reaction on specific chat bubble void setReaction({ required String emoji, diff --git a/lib/src/models/chat_bubble.dart b/lib/src/models/chat_bubble.dart index c1b05680..dbc9c9f4 100644 --- a/lib/src/models/chat_bubble.dart +++ b/lib/src/models/chat_bubble.dart @@ -52,8 +52,6 @@ class ChatBubble { /// time only final Function(Message message)? onMessageRead; - final Widget? messageTimeAndUnreadCount; - const ChatBubble({ this.color, this.borderRadius, @@ -64,6 +62,5 @@ class ChatBubble { this.senderNameTextStyle, this.receiptsWidgetConfig, this.onMessageRead, - this.messageTimeAndUnreadCount, }); } diff --git a/lib/src/models/config_models/profile_circle_configuration.dart b/lib/src/models/config_models/profile_circle_configuration.dart index e62c8974..f265bb2a 100644 --- a/lib/src/models/config_models/profile_circle_configuration.dart +++ b/lib/src/models/config_models/profile_circle_configuration.dart @@ -61,6 +61,9 @@ class ProfileCircleConfiguration { final NetworkImageProgressIndicatorBuilder? networkImageProgressIndicatorBuilder; + // custom profile avatar + final Widget? Function(ChatUser?)? profileAvatar; + const ProfileCircleConfiguration({ this.onAvatarTap, this.padding, @@ -73,5 +76,6 @@ class ProfileCircleConfiguration { this.networkImageErrorBuilder, this.assetImageErrorBuilder, this.networkImageProgressIndicatorBuilder, + this.profileAvatar, }); } diff --git a/lib/src/models/config_models/receipts_widget_config.dart b/lib/src/models/config_models/receipts_widget_config.dart index 4e53f8ce..8169f844 100644 --- a/lib/src/models/config_models/receipts_widget_config.dart +++ b/lib/src/models/config_models/receipts_widget_config.dart @@ -29,7 +29,7 @@ class ReceiptsWidgetConfig { /// Right now it's implemented to show animation only at the last message just /// like instagram. /// By default [sendMessageAnimationBuilder] - final Widget Function(MessageStatus status)? receiptsBuilder; + final Widget Function(Message message)? receiptsBuilder; /// Just like Instagram messages receipts are displayed at the bottom of last /// message. If in case you want to modify it using your custom widget you can diff --git a/lib/src/models/data_models/chat_user.dart b/lib/src/models/data_models/chat_user.dart index e8280195..a89043aa 100644 --- a/lib/src/models/data_models/chat_user.dart +++ b/lib/src/models/data_models/chat_user.dart @@ -31,11 +31,23 @@ class ChatUser { /// Provides name of user. final String name; + // Provides title of user + final String? title; + /// Provides profile picture as network URL or asset of user. /// Or /// Provides profile picture's data in base64 string. final String? profilePhoto; + /// Provides emoji of user + final String? emoji; + + /// Provides introduction of user + final String? introduction; + + /// Provides createdAt of user + final DateTime? createdAt; + /// Field to set default image if network url for profile image not provided final String defaultAvatarImage; @@ -55,7 +67,11 @@ class ChatUser { ChatUser({ required this.id, required this.name, + this.title, this.profilePhoto, + this.emoji, + this.introduction, + this.createdAt, this.defaultAvatarImage = profileImage, this.imageType = ImageType.network, this.assetImageErrorBuilder, @@ -66,7 +82,11 @@ class ChatUser { factory ChatUser.fromJson(Map json) => ChatUser( id: json["id"], name: json["name"], + title: json["title"], profilePhoto: json["profilePhoto"], + emoji: json["emoji"], + introduction: json["introduction"], + createdAt: json["createdAt"], imageType: ImageType.tryParse(json['imageType']?.toString()) ?? ImageType.network, defaultAvatarImage: json["defaultAvatarImage"], @@ -75,8 +95,12 @@ class ChatUser { Map toJson() => { 'id': id, 'name': name, + 'title': title, 'profilePhoto': profilePhoto, + 'emoji': emoji, + 'introduction': introduction, 'imageType': imageType.name, + 'createdAt': createdAt, 'defaultAvatarImage': defaultAvatarImage, }; @@ -84,14 +108,22 @@ class ChatUser { String? id, String? name, String? profilePhoto, + String? title, + String? emoji, + String? introduction, ImageType? imageType, + DateTime? createdAt, String? defaultAvatarImage, bool forceNullValue = false, }) { return ChatUser( id: id ?? this.id, name: name ?? this.name, + title: title ?? this.title, + emoji: emoji ?? this.emoji, + introduction: introduction ?? this.introduction, imageType: imageType ?? this.imageType, + createdAt: createdAt ?? this.createdAt, profilePhoto: forceNullValue ? profilePhoto : profilePhoto ?? this.profilePhoto, defaultAvatarImage: defaultAvatarImage ?? this.defaultAvatarImage, diff --git a/lib/src/models/data_models/message.dart b/lib/src/models/data_models/message.dart index ca04173a..5be12352 100644 --- a/lib/src/models/data_models/message.dart +++ b/lib/src/models/data_models/message.dart @@ -54,6 +54,12 @@ class Message { /// Provides max duration for recorded voice message. Duration? voiceMessageDuration; + // Provides unread count + int? unreadCount; + + // Provice custom data + Map? customData; + Message({ this.id = '', required this.message, @@ -64,6 +70,8 @@ class Message { this.messageType = MessageType.text, this.voiceMessageDuration, MessageStatus status = MessageStatus.pending, + this.customData, + this.unreadCount, }) : reaction = reaction ?? Reaction(reactions: [], reactedUserIds: []), key = GlobalKey(), _status = ValueNotifier(status), diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index aaa54665..e2e90ad0 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -122,18 +122,17 @@ class _ChatBubbleWidgetState extends State { padding: chatBubbleConfig?.padding ?? const EdgeInsets.only(left: 5.0), margin: chatBubbleConfig?.margin ?? const EdgeInsets.only(bottom: 10), width: double.infinity, - alignment: isMessageBySender ? Alignment.centerRight : Alignment.centerLeft, + alignment: + isMessageBySender ? Alignment.centerRight : Alignment.centerLeft, child: Row( mainAxisAlignment: isMessageBySender ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isMessageBySender && (featureActiveConfig?.enableOtherUserProfileAvatar ?? true)) profileCircle(messagedUser), - if (isMessageBySender) getReceipt(), _messagesWidgetColumn(messagedUser), - if (!isMessageBySender) getReceipt(), if (isMessageBySender && (featureActiveConfig?.enableCurrentUserProfileAvatar ?? true)) profileCircle(messagedUser), @@ -159,6 +158,8 @@ class _ChatBubbleWidgetState extends State { circleRadius: profileCircleConfig?.circleRadius, onTap: () => _onAvatarTap(messagedUser), onLongPress: () => _onAvatarLongPress(messagedUser), + profileAvatar: profileCircleConfig?.profileAvatar, + user: messagedUser, ); } @@ -193,10 +194,13 @@ class _ChatBubbleWidgetState extends State { } } - Widget getReceipt() { - final showReceipts = chatListConfig.chatBubbleConfig - ?.outgoingChatBubbleConfig?.receiptsWidgetConfig?.showReceiptsIn ?? - ShowReceiptsIn.lastMessage; + Widget getReceipt(bool isMessageBySender) { + final chatBubbleConfig = isMessageBySender + ? chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig + : chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig; + final showReceipts = + chatBubbleConfig?.receiptsWidgetConfig?.showReceiptsIn ?? + ShowReceiptsIn.lastMessage; if (showReceipts == ShowReceiptsIn.all) { return ValueListenableBuilder( valueListenable: widget.message.statusNotifier, @@ -205,10 +209,9 @@ class _ChatBubbleWidgetState extends State { ?.featureActiveConfig .receiptsBuilderVisibility ?? true) { - return chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig - ?.receiptsWidgetConfig?.receiptsBuilder - ?.call(value) ?? - sendMessageAnimationBuilder(value); + return chatBubbleConfig?.receiptsWidgetConfig?.receiptsBuilder + ?.call(widget.message) ?? + sendMessageAnimationBuilder(widget.message.status); } return const SizedBox(); }, @@ -222,12 +225,11 @@ class _ChatBubbleWidgetState extends State { ?.featureActiveConfig .receiptsBuilderVisibility ?? true) { - return chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig - ?.receiptsWidgetConfig?.receiptsBuilder - ?.call(value) ?? - sendMessageAnimationBuilder(value); + return chatBubbleConfig?.receiptsWidgetConfig?.receiptsBuilder + ?.call(widget.message) ?? + sendMessageAnimationBuilder(widget.message.status); } - return sendMessageAnimationBuilder(value); + return sendMessageAnimationBuilder(widget.message.status); }); } return const SizedBox(); @@ -249,13 +251,27 @@ class _ChatBubbleWidgetState extends State { !isMessageBySender && (featureActiveConfig?.enableOtherUserName ?? true)) Padding( - padding: chatListConfig - .chatBubbleConfig?.inComingChatBubbleConfig?.padding ?? - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Text( - messagedUser?.name ?? '', - style: chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig - ?.senderNameTextStyle, + padding: const EdgeInsets.only( + bottom: 6, + left: 12, + ), + child: Row( + children: [ + Text( + messagedUser?.name ?? '', + style: chatListConfig.chatBubbleConfig + ?.inComingChatBubbleConfig?.senderNameTextStyle, + ), + const SizedBox(width: 3), + Text( + messagedUser?.title != null ? '· ${messagedUser?.title}' : '', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xFF717171), + ), + ), + ], ), ), if (replyMessage.isNotEmpty) @@ -272,40 +288,47 @@ class _ChatBubbleWidgetState extends State { SwipeToReply( isMessageByCurrentUser: isMessageBySender, onSwipe: isMessageBySender ? onLeftSwipe : onRightSwipe, - child: MessageView( - outgoingChatBubbleConfig: - chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig, - isLongPressEnable: - (featureActiveConfig?.enableReactionPopup ?? true) || - (featureActiveConfig?.enableReplySnackBar ?? true), - inComingChatBubbleConfig: - chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig, - message: widget.message, - isMessageBySender: isMessageBySender, - messageConfig: chatListConfig.messageConfig, - onLongPress: widget.onLongPress, - chatBubbleMaxWidth: chatListConfig.chatBubbleConfig?.maxWidth, - longPressAnimationDuration: - chatListConfig.chatBubbleConfig?.longPressAnimationDuration, - onDoubleTap: featureActiveConfig?.enableDoubleTapToLike ?? false - ? chatListConfig.chatBubbleConfig?.onDoubleTap ?? - (message) => currentUser != null - ? chatController?.setReaction( - emoji: heart, - messageId: message.id, - userId: currentUser!.id, - ) - : null - : null, - shouldHighlight: widget.shouldHighlight, - controller: chatController, - highlightColor: chatListConfig.repliedMessageConfig - ?.repliedMsgAutoScrollConfig.highlightColor ?? - Colors.grey, - highlightScale: chatListConfig.repliedMessageConfig - ?.repliedMsgAutoScrollConfig.highlightScale ?? - 1.1, - onMaxDuration: _onMaxDuration, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (isMessageBySender) getReceipt(isMessageBySender), + MessageView( + outgoingChatBubbleConfig: + chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig, + isLongPressEnable: + (featureActiveConfig?.enableReactionPopup ?? true) || + (featureActiveConfig?.enableReplySnackBar ?? true), + inComingChatBubbleConfig: + chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig, + message: widget.message, + isMessageBySender: isMessageBySender, + messageConfig: chatListConfig.messageConfig, + onLongPress: widget.onLongPress, + chatBubbleMaxWidth: chatListConfig.chatBubbleConfig?.maxWidth, + longPressAnimationDuration: + chatListConfig.chatBubbleConfig?.longPressAnimationDuration, + onDoubleTap: featureActiveConfig?.enableDoubleTapToLike ?? false + ? chatListConfig.chatBubbleConfig?.onDoubleTap ?? + (message) => currentUser != null + ? chatController?.setReaction( + emoji: heart, + messageId: message.id, + userId: currentUser!.id, + ) + : null + : null, + shouldHighlight: widget.shouldHighlight, + controller: chatController, + highlightColor: chatListConfig.repliedMessageConfig + ?.repliedMsgAutoScrollConfig.highlightColor ?? + Colors.grey, + highlightScale: chatListConfig.repliedMessageConfig + ?.repliedMsgAutoScrollConfig.highlightScale ?? + 1.1, + onMaxDuration: _onMaxDuration, + ), + if (!isMessageBySender) getReceipt(isMessageBySender), + ], ), ), ], diff --git a/lib/src/widgets/chatui_textfield.dart b/lib/src/widgets/chatui_textfield.dart index ec6cf0bc..4905e315 100644 --- a/lib/src/widgets/chatui_textfield.dart +++ b/lib/src/widgets/chatui_textfield.dart @@ -224,76 +224,77 @@ class _ChatUITextFieldState extends State { const Icon(Icons.send), ); } else { - return Row( + return const Row( children: [ - if (!isRecordingValue) ...[ - if (sendMessageConfig?.enableCameraImagePicker ?? - true) - IconButton( - constraints: const BoxConstraints(), - onPressed: (textFieldConfig?.enabled ?? true) - ? () => _onIconPressed( - ImageSource.camera, - config: sendMessageConfig - ?.imagePickerConfiguration, - ) - : null, - icon: imagePickerIconsConfig - ?.cameraImagePickerIcon ?? - Icon( - Icons.camera_alt_outlined, - color: - imagePickerIconsConfig?.cameraIconColor, - ), - ), - if (sendMessageConfig?.enableGalleryImagePicker ?? - true) - IconButton( - constraints: const BoxConstraints(), - onPressed: (textFieldConfig?.enabled ?? true) - ? () => _onIconPressed( - ImageSource.gallery, - config: sendMessageConfig - ?.imagePickerConfiguration, - ) - : null, - icon: imagePickerIconsConfig - ?.galleryImagePickerIcon ?? - Icon( - Icons.image, - color: imagePickerIconsConfig - ?.galleryIconColor, - ), - ), - ], - if ((sendMessageConfig?.allowRecordingVoice ?? false) && - !kIsWeb && - (Platform.isIOS || Platform.isAndroid)) - IconButton( - onPressed: (textFieldConfig?.enabled ?? true) - ? _recordOrStop - : null, - icon: (isRecordingValue - ? voiceRecordingConfig?.stopIcon - : voiceRecordingConfig?.micIcon) ?? - Icon( - isRecordingValue ? Icons.stop : Icons.mic, - color: - voiceRecordingConfig?.recorderIconColor, - ), - ), - if (isRecordingValue && - cancelRecordConfiguration != null) - IconButton( - onPressed: () { - cancelRecordConfiguration?.onCancel?.call(); - _cancelRecording(); - }, - icon: cancelRecordConfiguration?.icon ?? - const Icon(Icons.cancel_outlined), - color: cancelRecordConfiguration?.iconColor ?? - voiceRecordingConfig?.recorderIconColor, - ), + // if (!isRecordingValue) ...[ + // if (sendMessageConfig?.enableCameraImagePicker ?? + // true) + // IconButton( + // constraints: const BoxConstraints(), + // onPressed: (textFieldConfig?.enabled ?? true) + // ? () => _onIconPressed( + // ImageSource.camera, + // config: sendMessageConfig + // ?.imagePickerConfiguration, + // ) + // : null, + // icon: imagePickerIconsConfig + // ?.cameraImagePickerIcon ?? + // Icon( + // Icons.camera_alt_outlined, + // color: imagePickerIconsConfig + // ?.cameraIconColor, + // ), + // ), + // if (sendMessageConfig?.enableGalleryImagePicker ?? + // true) + // IconButton( + // constraints: const BoxConstraints(), + // onPressed: (textFieldConfig?.enabled ?? true) + // ? () => _onIconPressed( + // ImageSource.gallery, + // config: sendMessageConfig + // ?.imagePickerConfiguration, + // ) + // : null, + // icon: imagePickerIconsConfig + // ?.galleryImagePickerIcon ?? + // Icon( + // Icons.image, + // color: imagePickerIconsConfig + // ?.galleryIconColor, + // ), + // ), + // ], + // if ((sendMessageConfig?.allowRecordingVoice ?? + // false) && + // !kIsWeb && + // (Platform.isIOS || Platform.isAndroid)) + // IconButton( + // onPressed: (textFieldConfig?.enabled ?? true) + // ? _recordOrStop + // : null, + // icon: (isRecordingValue + // ? voiceRecordingConfig?.stopIcon + // : voiceRecordingConfig?.micIcon) ?? + // Icon( + // isRecordingValue ? Icons.stop : Icons.mic, + // color: voiceRecordingConfig + // ?.recorderIconColor, + // ), + // ), + // if (isRecordingValue && + // cancelRecordConfiguration != null) + // IconButton( + // onPressed: () { + // cancelRecordConfiguration?.onCancel?.call(); + // _cancelRecording(); + // }, + // icon: cancelRecordConfiguration?.icon ?? + // const Icon(Icons.cancel_outlined), + // color: cancelRecordConfiguration?.iconColor ?? + // voiceRecordingConfig?.recorderIconColor, + // ), ], ); } diff --git a/lib/src/widgets/link_preview.dart b/lib/src/widgets/link_preview.dart index ba19e79d..d44f9e4c 100644 --- a/lib/src/widgets/link_preview.dart +++ b/lib/src/widgets/link_preview.dart @@ -51,31 +51,28 @@ class LinkPreview extends StatelessWidget { children: [ if (!url.isImageUrl && !(context.chatBubbleConfig?.disableLinkPreview ?? false)) ...{ - Padding( - padding: const EdgeInsets.symmetric(vertical: verticalPadding), - child: AnyLinkPreview( - link: url, - removeElevation: true, - errorBody: linkPreviewConfig?.errorBody, - proxyUrl: linkPreviewConfig?.proxyUrl, - onTap: _onLinkTap, - placeholderWidget: SizedBox( - height: MediaQuery.of(context).size.height * 0.25, - width: double.infinity, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 1, - color: linkPreviewConfig?.loadingColor, - ), + AnyLinkPreview( + link: url, + removeElevation: true, + errorBody: linkPreviewConfig?.errorBody, + proxyUrl: linkPreviewConfig?.proxyUrl, + onTap: _onLinkTap, + placeholderWidget: SizedBox( + height: MediaQuery.of(context).size.height * 0.25, + width: double.infinity, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 1, + color: linkPreviewConfig?.loadingColor, ), ), - backgroundColor: - linkPreviewConfig?.backgroundColor ?? Colors.grey.shade200, - borderRadius: linkPreviewConfig?.borderRadius, - bodyStyle: linkPreviewConfig?.bodyStyle ?? - const TextStyle(color: Colors.black), - titleStyle: linkPreviewConfig?.titleStyle, ), + backgroundColor: + linkPreviewConfig?.backgroundColor ?? Colors.grey.shade200, + borderRadius: linkPreviewConfig?.borderRadius, + bodyStyle: linkPreviewConfig?.bodyStyle ?? + const TextStyle(color: Colors.black), + titleStyle: linkPreviewConfig?.titleStyle, ), } else ...{ Padding( @@ -91,18 +88,17 @@ class LinkPreview extends StatelessWidget { ), ), }, - const SizedBox(height: verticalPadding), - InkWell( - onTap: _onLinkTap, - child: Text( - url, - style: linkPreviewConfig?.linkStyle ?? - const TextStyle( - color: Colors.white, - decoration: TextDecoration.underline, - ), - ), - ), + // InkWell( + // onTap: _onLinkTap, + // child: Text( + // url, + // style: linkPreviewConfig?.linkStyle ?? + // const TextStyle( + // color: Colors.white, + // decoration: TextDecoration.underline, + // ), + // ), + // ), ], ), ); diff --git a/lib/src/widgets/profile_circle.dart b/lib/src/widgets/profile_circle.dart index c113a9ca..ad5e6ab8 100644 --- a/lib/src/widgets/profile_circle.dart +++ b/lib/src/widgets/profile_circle.dart @@ -26,8 +26,11 @@ import '../utils/constants/constants.dart'; import '../values/enumeration.dart'; import '../values/typedefs.dart'; import 'profile_image_widget.dart'; +import '../../../chatview.dart'; class ProfileCircle extends StatelessWidget { + final ChatUser? user; + const ProfileCircle({ Key? key, required this.bottomPadding, @@ -41,6 +44,8 @@ class ProfileCircle extends StatelessWidget { this.networkImageErrorBuilder, this.imageType = ImageType.network, this.networkImageProgressIndicatorBuilder, + this.profileAvatar, + this.user, }) : super(key: key); /// Allow users to give default bottom padding according to user case. @@ -79,6 +84,9 @@ class ProfileCircle extends StatelessWidget { final NetworkImageProgressIndicatorBuilder? networkImageProgressIndicatorBuilder; + /// custom profile avatar + final Widget? Function(ChatUser?)? profileAvatar; + @override Widget build(BuildContext context) { return Padding( @@ -87,16 +95,17 @@ class ProfileCircle extends StatelessWidget { child: InkWell( onLongPress: onLongPress, onTap: onTap, - child: ProfileImageWidget( - circleRadius: circleRadius ?? 16, - imageUrl: imageUrl, - defaultAvatarImage: defaultAvatarImage, - assetImageErrorBuilder: assetImageErrorBuilder, - networkImageErrorBuilder: networkImageErrorBuilder, - imageType: imageType, - networkImageProgressIndicatorBuilder: - networkImageProgressIndicatorBuilder, - ), + child: profileAvatar?.call(user) ?? + ProfileImageWidget( + circleRadius: circleRadius ?? 16, + imageUrl: imageUrl, + defaultAvatarImage: defaultAvatarImage, + assetImageErrorBuilder: assetImageErrorBuilder, + networkImageErrorBuilder: networkImageErrorBuilder, + imageType: imageType, + networkImageProgressIndicatorBuilder: + networkImageProgressIndicatorBuilder, + ), ), ); } diff --git a/lib/src/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index e284517c..e04f0ba5 100644 --- a/lib/src/widgets/send_message_widget.dart +++ b/lib/src/widgets/send_message_widget.dart @@ -101,162 +101,156 @@ class SendMessageWidgetState extends State { chatListConfig.scrollToBottomButtonConfig; return Align( alignment: Alignment.bottomCenter, - child: widget.sendMessageBuilder != null - ? widget.sendMessageBuilder!(replyMessage) - : SizedBox( - width: MediaQuery.of(context).size.width, - child: Stack( + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Stack( + children: [ + // This has been added to prevent messages from being + // displayed below the text field + // when the user scrolls the message list. + Positioned( + right: 0, + left: 0, + bottom: 0, + child: Container( + height: MediaQuery.of(context).size.height / + ((!kIsWeb && Platform.isIOS) ? 24 : 28), + color: chatListConfig.chatBackgroundConfig.backgroundColor ?? + Colors.white, + ), + ), + Positioned( + right: 0, + left: 0, + bottom: 0, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - // This has been added to prevent messages from being - // displayed below the text field - // when the user scrolls the message list. - Positioned( - right: 0, - left: 0, - bottom: 0, - child: Container( - height: MediaQuery.of(context).size.height / - ((!kIsWeb && Platform.isIOS) ? 24 : 28), - color: - chatListConfig.chatBackgroundConfig.backgroundColor ?? - Colors.white, + if (chatViewIW + ?.featureActiveConfig.enableScrollToBottomButton ?? + true) + Align( + alignment: + scrollToBottomButtonConfig?.alignment?.alignment ?? + Alignment.bottomCenter, + child: Padding( + padding: scrollToBottomButtonConfig?.padding ?? + EdgeInsets.zero, + child: const ScrollToBottomButton(), + ), ), - ), - Positioned( - right: 0, - left: 0, - bottom: 0, - child: Column( - mainAxisSize: MainAxisSize.min, + Padding( + key: chatViewIW?.chatTextFieldViewKey, + padding: EdgeInsets.fromLTRB( + bottomPadding4, + bottomPadding4, + bottomPadding4, + _bottomPadding, + ), + child: Stack( + alignment: Alignment.bottomCenter, children: [ - if (chatViewIW?.featureActiveConfig - .enableScrollToBottomButton ?? - true) - Align( - alignment: scrollToBottomButtonConfig - ?.alignment?.alignment ?? - Alignment.bottomCenter, - child: Padding( - padding: scrollToBottomButtonConfig?.padding ?? - EdgeInsets.zero, - child: const ScrollToBottomButton(), - ), - ), - Padding( - key: chatViewIW?.chatTextFieldViewKey, - padding: EdgeInsets.fromLTRB( - bottomPadding4, - bottomPadding4, - bottomPadding4, - _bottomPadding, - ), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - ValueListenableBuilder( - builder: (_, state, child) { - final replyTitle = - "${PackageStrings.replyTo} $_replyTo"; - if (state.message.isNotEmpty) { - return widget.replyMessageBuilder - ?.call(context, state) ?? - Container( - decoration: BoxDecoration( - color: widget.sendMessageConfig - ?.textFieldBackgroundColor ?? - Colors.white, - borderRadius: - const BorderRadius.vertical( - top: Radius.circular(14), - ), - ), - margin: const EdgeInsets.only( - bottom: 17, - right: 0.4, - left: 0.4, - ), - padding: const EdgeInsets.fromLTRB( - leftPadding, - leftPadding, - leftPadding, - 30, - ), - child: Container( - margin: const EdgeInsets.only( - bottom: 2), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 6, - ), - decoration: BoxDecoration( - color: widget.sendMessageConfig - ?.replyDialogColor ?? - Colors.grey.shade200, - borderRadius: - BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Expanded( - child: Text( - replyTitle, - maxLines: 1, - overflow: TextOverflow - .ellipsis, - style: TextStyle( - color: widget - .sendMessageConfig - ?.replyTitleColor ?? - Colors.deepPurple, - fontWeight: - FontWeight.bold, - letterSpacing: 0.25, - ), - ), - ), - IconButton( - constraints: - const BoxConstraints(), - padding: EdgeInsets.zero, - icon: Icon( - Icons.close, - color: widget - .sendMessageConfig - ?.closeIconColor ?? - Colors.black, - size: 16, - ), - onPressed: onCloseTap, - ), - ], + ValueListenableBuilder( + builder: (_, state, child) { + final replyTitle = + "${PackageStrings.replyTo} $_replyTo"; + if (state.message.isNotEmpty) { + return widget.replyMessageBuilder + ?.call(context, state) ?? + Container( + decoration: BoxDecoration( + color: widget.sendMessageConfig + ?.textFieldBackgroundColor ?? + Colors.white, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(14), + ), + ), + margin: const EdgeInsets.only( + bottom: 17, + right: 0.4, + left: 0.4, + ), + padding: const EdgeInsets.fromLTRB( + leftPadding, + leftPadding, + leftPadding, + 30, + ), + child: Container( + margin: const EdgeInsets.only(bottom: 2), + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 6, + ), + decoration: BoxDecoration( + color: widget.sendMessageConfig + ?.replyDialogColor ?? + Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + replyTitle, + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: TextStyle( + color: widget + .sendMessageConfig + ?.replyTitleColor ?? + Colors.deepPurple, + fontWeight: FontWeight.bold, + letterSpacing: 0.25, + ), ), - ReplyMessageView( - message: state, - customMessageReplyViewBuilder: - widget.messageConfig - ?.customMessageReplyViewBuilder, - sendMessageConfig: - widget.sendMessageConfig, + ), + IconButton( + constraints: + const BoxConstraints(), + padding: EdgeInsets.zero, + icon: Icon( + Icons.close, + color: widget + .sendMessageConfig + ?.closeIconColor ?? + Colors.black, + size: 16, ), - ], - ), + onPressed: onCloseTap, + ), + ], + ), + ReplyMessageView( + message: state, + customMessageReplyViewBuilder: widget + .messageConfig + ?.customMessageReplyViewBuilder, + sendMessageConfig: + widget.sendMessageConfig, ), - ); - } else { - return const SizedBox.shrink(); - } - }, - valueListenable: _replyMessage, - ), - ChatUITextField( + ], + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + valueListenable: _replyMessage, + ), + widget.sendMessageBuilder != null + ? widget.sendMessageBuilder!(replyMessage) + : ChatUITextField( focusNode: _focusNode, textEditingController: _textEditingController, onPressed: _onPressed, @@ -264,15 +258,15 @@ class SendMessageWidgetState extends State { onRecordingComplete: _onRecordingComplete, onImageSelected: _onImageSelected, ) - ], - ), - ), ], ), ), ], ), ), + ], + ), + ), ); } diff --git a/lib/src/widgets/text_message_view.dart b/lib/src/widgets/text_message_view.dart index 4dacc90b..4ee7ef7a 100644 --- a/lib/src/widgets/text_message_view.dart +++ b/lib/src/widgets/text_message_view.dart @@ -72,36 +72,42 @@ class TextMessageView extends StatelessWidget { return Stack( clipBehavior: Clip.none, children: [ - Container( - constraints: BoxConstraints( - maxWidth: chatBubbleMaxWidth ?? - MediaQuery.of(context).size.width * 0.75), - padding: _padding ?? - const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - margin: _margin ?? - EdgeInsets.fromLTRB( - 5, 0, 6, message.reaction.reactions.isNotEmpty ? 15 : 2), - decoration: BoxDecoration( - color: highlightMessage ? highlightColor : _color, - borderRadius: _borderRadius(textMessage), - ), - child: textMessage.isUrl - ? LinkPreview( - linkPreviewConfig: _linkPreviewConfig, - url: textMessage, - ) - : Text( - textMessage, - style: _textStyle ?? - textTheme.bodyMedium!.copyWith( - color: Colors.white, - fontSize: 16, - ), + if (textMessage.isUrl) + Container( + constraints: BoxConstraints( + maxWidth: chatBubbleMaxWidth ?? + MediaQuery.of(context).size.width * 0.75), + child: LinkPreview( + linkPreviewConfig: _linkPreviewConfig, + url: textMessage, + ), + ) + else + Container( + constraints: BoxConstraints( + maxWidth: chatBubbleMaxWidth ?? + MediaQuery.of(context).size.width * 0.75), + padding: _padding ?? + const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, ), - ), + margin: _margin ?? + EdgeInsets.fromLTRB( + 5, 0, 6, message.reaction.reactions.isNotEmpty ? 15 : 2), + decoration: BoxDecoration( + color: highlightMessage ? highlightColor : _color, + borderRadius: _borderRadius(textMessage), + ), + child: Text( + textMessage, + style: _textStyle ?? + textTheme.bodyMedium!.copyWith( + color: Colors.white, + fontSize: 16, + ), + ), + ), if (message.reaction.reactions.isNotEmpty) ReactionWidget( key: key, From 84c1c905ea90770def3b159611c7075d2669dbe1 Mon Sep 17 00:00:00 2001 From: bmlee Date: Fri, 31 Jan 2025 16:43:50 +0900 Subject: [PATCH 08/26] =?UTF-8?q?user=20type=20UI=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/models/data_models/chat_user.dart | 12 ++++++++ lib/src/widgets/chat_bubble_widget.dart | 37 ++++++++++++++++++----- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/src/models/data_models/chat_user.dart b/lib/src/models/data_models/chat_user.dart index a89043aa..a47cb2d9 100644 --- a/lib/src/models/data_models/chat_user.dart +++ b/lib/src/models/data_models/chat_user.dart @@ -25,6 +25,10 @@ import '../../values/enumeration.dart'; import '../../values/typedefs.dart'; class ChatUser { + static const String TYPE_USER = 'user'; + static const String TYPE_ADMIN = 'admin'; + static const String TYPE_BOT = 'bot'; + /// Provides id of user. final String id; @@ -48,6 +52,9 @@ class ChatUser { /// Provides createdAt of user final DateTime? createdAt; + /// Provides senderType of user + final String? type; + /// Field to set default image if network url for profile image not provided final String defaultAvatarImage; @@ -72,6 +79,7 @@ class ChatUser { this.emoji, this.introduction, this.createdAt, + this.type, this.defaultAvatarImage = profileImage, this.imageType = ImageType.network, this.assetImageErrorBuilder, @@ -87,6 +95,7 @@ class ChatUser { emoji: json["emoji"], introduction: json["introduction"], createdAt: json["createdAt"], + type: json["type"], imageType: ImageType.tryParse(json['imageType']?.toString()) ?? ImageType.network, defaultAvatarImage: json["defaultAvatarImage"], @@ -102,6 +111,7 @@ class ChatUser { 'imageType': imageType.name, 'createdAt': createdAt, 'defaultAvatarImage': defaultAvatarImage, + 'type': type, }; ChatUser copyWith({ @@ -114,6 +124,7 @@ class ChatUser { ImageType? imageType, DateTime? createdAt, String? defaultAvatarImage, + String? type, bool forceNullValue = false, }) { return ChatUser( @@ -127,6 +138,7 @@ class ChatUser { profilePhoto: forceNullValue ? profilePhoto : profilePhoto ?? this.profilePhoto, defaultAvatarImage: defaultAvatarImage ?? this.defaultAvatarImage, + type: type ?? this.type, ); } } diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index e2e90ad0..7a81255c 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -263,14 +263,37 @@ class _ChatBubbleWidgetState extends State { ?.inComingChatBubbleConfig?.senderNameTextStyle, ), const SizedBox(width: 3), - Text( - messagedUser?.title != null ? '· ${messagedUser?.title}' : '', - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Color(0xFF717171), + if (messagedUser?.type == ChatUser.TYPE_USER) + Text( + messagedUser?.title != null + ? '· ${messagedUser?.title}' + : '', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xFF717171), + ), + ), + if (messagedUser?.type == ChatUser.TYPE_BOT) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 3, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0xFF475467), + borderRadius: BorderRadius.circular(100), + ), + child: Text( + messagedUser!.type!, + style: const TextStyle( + fontSize: 9, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), ), - ), ], ), ), From 159e41ac227b7f6c46881dd0ec2d7e7626b8b557 Mon Sep 17 00:00:00 2001 From: bmlee Date: Fri, 31 Jan 2025 23:15:21 +0900 Subject: [PATCH 09/26] myReactionCountTextStyle --- .../message_reaction_configuration.dart | 4 + lib/src/widgets/chat_bubble_widget.dart | 20 ++++- lib/src/widgets/image_message_view.dart | 8 ++ lib/src/widgets/message_view.dart | 7 +- lib/src/widgets/reaction_widget.dart | 80 +++++++------------ lib/src/widgets/text_message_view.dart | 7 ++ lib/src/widgets/voice_message_view.dart | 6 ++ 7 files changed, 78 insertions(+), 54 deletions(-) diff --git a/lib/src/models/config_models/message_reaction_configuration.dart b/lib/src/models/config_models/message_reaction_configuration.dart index 22241a8f..f4de4cb8 100644 --- a/lib/src/models/config_models/message_reaction_configuration.dart +++ b/lib/src/models/config_models/message_reaction_configuration.dart @@ -51,6 +51,9 @@ class MessageReactionConfiguration { /// Used for giving text style to total count of reaction text. final TextStyle? reactionCountTextStyle; + /// Used for giving text style to total count of reaction text. + final TextStyle? myReactionCountTextStyle; + /// Provides configurations for reaction bottom sheet which shows reacted users /// and their reaction on any message. final ReactionsBottomSheetConfiguration? reactionsBottomSheetConfig; @@ -65,6 +68,7 @@ class MessageReactionConfiguration { this.reactionsBottomSheetConfig, this.reactionCountTextStyle, this.reactedUserCountTextStyle, + this.myReactionCountTextStyle, this.reactionSize, this.margin, this.padding, diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index 7a81255c..433a09f9 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -278,7 +278,7 @@ class _ChatBubbleWidgetState extends State { Container( padding: const EdgeInsets.symmetric( horizontal: 5, - vertical: 3, + vertical: 2, ), alignment: Alignment.center, decoration: BoxDecoration( @@ -314,7 +314,14 @@ class _ChatBubbleWidgetState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (isMessageBySender) getReceipt(isMessageBySender), + if (isMessageBySender) + Padding( + padding: EdgeInsets.only( + bottom: + widget.message.reaction.reactions.isNotEmpty ? 18 : 0, + ), + child: getReceipt(isMessageBySender), + ), MessageView( outgoingChatBubbleConfig: chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig, @@ -350,7 +357,14 @@ class _ChatBubbleWidgetState extends State { 1.1, onMaxDuration: _onMaxDuration, ), - if (!isMessageBySender) getReceipt(isMessageBySender), + if (!isMessageBySender) + Padding( + padding: EdgeInsets.only( + bottom: + widget.message.reaction.reactions.isNotEmpty ? 18 : 0, + ), + child: getReceipt(isMessageBySender), + ), ], ), ), diff --git a/lib/src/widgets/image_message_view.dart b/lib/src/widgets/image_message_view.dart index 2464398e..08e9a307 100644 --- a/lib/src/widgets/image_message_view.dart +++ b/lib/src/widgets/image_message_view.dart @@ -22,6 +22,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:chatview/chatview.dart'; import 'package:chatview/src/extensions/extensions.dart'; import 'package:chatview/src/models/models.dart'; import 'package:flutter/material.dart'; @@ -38,6 +39,7 @@ class ImageMessageView extends StatelessWidget { this.messageReactionConfig, this.highlightImage = false, this.highlightScale = 1.2, + this.controller, }) : super(key: key); /// Provides message instance of chat. @@ -58,6 +60,9 @@ class ImageMessageView extends StatelessWidget { /// Provides scale of highlighted image when user taps on replied image. final double highlightScale; + /// chat controller + final ChatController? controller; + String get imageUrl => message.message; Widget get iconButton => ShareIcon( @@ -75,6 +80,7 @@ class ImageMessageView extends StatelessWidget { if (isMessageBySender && !(imageMessageConfig?.hideShareIcon ?? false)) iconButton, Stack( + clipBehavior: Clip.none, children: [ GestureDetector( onTap: () => imageMessageConfig?.onTap != null @@ -139,6 +145,8 @@ class ImageMessageView extends StatelessWidget { isMessageBySender: isMessageBySender, reaction: message.reaction, messageReactionConfig: messageReactionConfig, + isMyReaction: message.reaction.reactedUserIds + .contains(controller?.currentUser.id), ), ], ), diff --git a/lib/src/widgets/message_view.dart b/lib/src/widgets/message_view.dart index 2d1215f5..70cc65da 100644 --- a/lib/src/widgets/message_view.dart +++ b/lib/src/widgets/message_view.dart @@ -159,7 +159,7 @@ class _MessageViewState extends State final emojiMessageConfiguration = messageConfig?.emojiMessageConfig; return Padding( padding: EdgeInsets.only( - bottom: widget.message.reaction.reactions.isNotEmpty ? 6 : 0, + bottom: widget.message.reaction.reactions.isNotEmpty ? 20 : 0, ), child: Column( crossAxisAlignment: CrossAxisAlignment.end, @@ -196,6 +196,8 @@ class _MessageViewState extends State messageReactionConfig: messageConfig?.messageReactionConfig, isMessageBySender: widget.isMessageBySender, + isMyReaction: widget.message.reaction.reactedUserIds + .contains(widget.controller?.currentUser.id), ), ], ); @@ -207,6 +209,7 @@ class _MessageViewState extends State messageReactionConfig: messageConfig?.messageReactionConfig, highlightImage: widget.shouldHighlight, highlightScale: widget.highlightScale, + controller: widget.controller, ); } else if (widget.message.messageType.isText) { return TextMessageView( @@ -218,6 +221,7 @@ class _MessageViewState extends State messageReactionConfig: messageConfig?.messageReactionConfig, highlightColor: widget.highlightColor, highlightMessage: widget.shouldHighlight, + controller: widget.controller, ); } else if (widget.message.messageType.isVoice) { return VoiceMessageView( @@ -229,6 +233,7 @@ class _MessageViewState extends State messageReactionConfig: messageConfig?.messageReactionConfig, inComingChatBubbleConfig: widget.inComingChatBubbleConfig, outgoingChatBubbleConfig: widget.outgoingChatBubbleConfig, + controller: widget.controller, ); } else if (widget.message.messageType.isCustom && messageConfig?.customMessageBuilder != null) { diff --git a/lib/src/widgets/reaction_widget.dart b/lib/src/widgets/reaction_widget.dart index 8d1ed9d6..fc59f02a 100644 --- a/lib/src/widgets/reaction_widget.dart +++ b/lib/src/widgets/reaction_widget.dart @@ -21,7 +21,6 @@ */ import 'package:chatview/src/extensions/extensions.dart'; import 'package:chatview/src/utils/measure_size.dart'; -import 'package:chatview/src/widgets/reactions_bottomsheet.dart'; import 'package:flutter/material.dart'; import '../../chatview.dart'; @@ -32,6 +31,7 @@ class ReactionWidget extends StatefulWidget { required this.reaction, this.messageReactionConfig, required this.isMessageBySender, + required this.isMyReaction, }) : super(key: key); /// Provides reaction instance of message. @@ -43,6 +43,9 @@ class ReactionWidget extends StatefulWidget { /// Represents current message is sent by current user. final bool isMessageBySender; + /// is my reaction + final bool isMyReaction; + @override State createState() => _ReactionWidgetState(); } @@ -68,23 +71,25 @@ class _ReactionWidgetState extends State { //// Convert into set to remove reduntant values final reactionsSet = widget.reaction.reactions.toSet(); return Positioned( - bottom: 0, - right: widget.isMessageBySender && needToExtend ? 0 : null, + bottom: -20, + right: widget.isMessageBySender ? null : 4, + left: widget.isMessageBySender ? 4 : null, child: InkWell( - onTap: () => chatController != null - ? ReactionsBottomSheet().show( - context: context, - reaction: widget.reaction, - chatController: chatController!, - reactionsBottomSheetConfig: - messageReactionConfig?.reactionsBottomSheetConfig, - ) - : null, + // onTap: () => chatController != null + // ? ReactionsBottomSheet().show( + // context: context, + // reaction: widget.reaction, + // chatController: chatController!, + // reactionsBottomSheetConfig: + // messageReactionConfig?.reactionsBottomSheetConfig, + // ) + // : null, child: MeasureSize( onSizeChange: (extend) => setState(() => needToExtend = extend), child: Container( + alignment: Alignment.center, padding: messageReactionConfig?.padding ?? - const EdgeInsets.symmetric(vertical: 1.7, horizontal: 6), + const EdgeInsets.symmetric(vertical: 5, horizontal: 7), margin: messageReactionConfig?.margin ?? EdgeInsets.only( left: widget.isMessageBySender ? 10 : 16, @@ -101,50 +106,25 @@ class _ReactionWidgetState extends State { ), ), child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - reactionsSet.join(' '), + reactionsSet.join(''), style: TextStyle( fontSize: messageReactionConfig?.reactionSize ?? 13, ), ), - if (chatController?.otherUsers.isNotEmpty ?? false) ...[ - if (!(widget.reaction.reactedUserIds.length > 3) && - !(reactionsSet.length > 1)) - ...List.generate( - widget.reaction.reactedUserIds.length, - (reactedUserIndex) => widget - .reaction.reactedUserIds[reactedUserIndex] - .getUserProfilePicture( - getChatUser: (userId) => - chatController?.getUserFromId(userId), - profileCirclePadding: - messageReactionConfig?.profileCirclePadding, - profileCircleRadius: - messageReactionConfig?.profileCircleRadius, - ), - ), - if (widget.reaction.reactedUserIds.length > 3 && - !(reactionsSet.length > 1)) - Padding( - padding: const EdgeInsets.only(left: 2), - child: Text( - '+${widget.reaction.reactedUserIds.length}', - style: - messageReactionConfig?.reactedUserCountTextStyle ?? - _reactionTextStyle, - ), - ), - if (reactionsSet.length > 1) - Padding( - padding: const EdgeInsets.only(left: 2), - child: Text( - widget.reaction.reactedUserIds.length.toString(), - style: messageReactionConfig?.reactionCountTextStyle ?? + Padding( + padding: const EdgeInsets.only(left: 3), + child: Text( + widget.reaction.reactedUserIds.length.toString(), + style: widget.isMyReaction + ? messageReactionConfig?.myReactionCountTextStyle ?? + _reactionTextStyle + : messageReactionConfig?.reactionCountTextStyle ?? _reactionTextStyle, - ), - ), - ], + ), + ), ], ), ), diff --git a/lib/src/widgets/text_message_view.dart b/lib/src/widgets/text_message_view.dart index 4ee7ef7a..7f653d6b 100644 --- a/lib/src/widgets/text_message_view.dart +++ b/lib/src/widgets/text_message_view.dart @@ -19,6 +19,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +import 'package:chatview/chatview.dart'; import 'package:flutter/material.dart'; import 'package:chatview/src/extensions/extensions.dart'; @@ -39,6 +40,7 @@ class TextMessageView extends StatelessWidget { this.messageReactionConfig, this.highlightMessage = false, this.highlightColor, + this.controller, }) : super(key: key); /// Represents current message is sent by current user. @@ -65,6 +67,9 @@ class TextMessageView extends StatelessWidget { /// Allow user to set color of highlighted message. final Color? highlightColor; + /// chat controller + final ChatController? controller; + @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; @@ -114,6 +119,8 @@ class TextMessageView extends StatelessWidget { isMessageBySender: isMessageBySender, reaction: message.reaction, messageReactionConfig: messageReactionConfig, + isMyReaction: message.reaction.reactedUserIds + .contains(controller?.currentUser.id), ), ], ); diff --git a/lib/src/widgets/voice_message_view.dart b/lib/src/widgets/voice_message_view.dart index c8ce902a..a2b02bfc 100644 --- a/lib/src/widgets/voice_message_view.dart +++ b/lib/src/widgets/voice_message_view.dart @@ -17,6 +17,7 @@ class VoiceMessageView extends StatefulWidget { this.onMaxDuration, this.messageReactionConfig, this.config, + this.controller, }) : super(key: key); /// Provides configuration related to voice message. @@ -41,6 +42,9 @@ class VoiceMessageView extends StatefulWidget { /// Provides configuration of chat bubble appearance from current user of chat. final ChatBubble? outgoingChatBubbleConfig; + /// chat controller + final ChatController? controller; + @override State createState() => _VoiceMessageViewState(); } @@ -143,6 +147,8 @@ class _VoiceMessageViewState extends State { isMessageBySender: widget.isMessageBySender, reaction: widget.message.reaction, messageReactionConfig: widget.messageReactionConfig, + isMyReaction: widget.message.reaction.reactedUserIds + .contains(widget.controller?.currentUser.id), ), ], ); From 783f7bd9f57321271bfbfdb9273a2cfaa3d4e6d8 Mon Sep 17 00:00:00 2001 From: bmlee Date: Sat, 1 Feb 2025 18:40:47 +0900 Subject: [PATCH 10/26] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=A2=8C=EC=9A=B0=20=ED=8C=A8=EB=94=A9=200=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/send_message_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index e04f0ba5..10c880f3 100644 --- a/lib/src/widgets/send_message_widget.dart +++ b/lib/src/widgets/send_message_widget.dart @@ -142,9 +142,9 @@ class SendMessageWidgetState extends State { Padding( key: chatViewIW?.chatTextFieldViewKey, padding: EdgeInsets.fromLTRB( + 0, bottomPadding4, - bottomPadding4, - bottomPadding4, + 0, _bottomPadding, ), child: Stack( From 602033a6203c7d4aa01ecfc3c170e91f1b6e2bec Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 2 Feb 2025 19:51:39 +0900 Subject: [PATCH 11/26] =?UTF-8?q?loadMorea=20=EC=9D=B8=EB=94=94=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20opacity=20=EC=95=A0=EB=8B=88=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/chat_list_widget.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/chat_list_widget.dart b/lib/src/widgets/chat_list_widget.dart index 22306eb2..efb1fb56 100644 --- a/lib/src/widgets/chat_list_widget.dart +++ b/lib/src/widgets/chat_list_widget.dart @@ -116,19 +116,19 @@ class _ChatListWidgetState extends State children: [ ValueListenableBuilder( valueListenable: _isNextPageLoading, - builder: (_, isNextPageLoading, child) { - if (isNextPageLoading && - (featureActiveConfig?.enablePagination ?? false)) { - return SizedBox( - height: Scaffold.of(context).appBarMaxHeight, + builder: + (BuildContext context, bool isNextPageLoading, Widget? child) { + return SizedBox( + height: isNextPageLoading ? 50.0 : 0.0, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: isNextPageLoading ? 1.0 : 0.0, child: Center( child: widget.loadingWidget ?? const CircularProgressIndicator(), ), - ); - } else { - return const SizedBox.shrink(); - } + ), + ); }, ), Expanded( From 69d44316ae8742aa6c4c6e245c902af97fda2174 Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 2 Feb 2025 20:34:28 +0900 Subject: [PATCH 12/26] =?UTF-8?q?custom=20SendMessageBuilder=EC=97=90=20fo?= =?UTF-8?q?cusNode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/values/typedefs.dart | 1 + lib/src/widgets/chat_bubble_widget.dart | 5 +++-- lib/src/widgets/send_message_widget.dart | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/src/values/typedefs.dart b/lib/src/values/typedefs.dart index 1f5ee940..7c3ea199 100644 --- a/lib/src/values/typedefs.dart +++ b/lib/src/values/typedefs.dart @@ -31,6 +31,7 @@ typedef StringMessageCallBack = void Function( ); typedef ReplyMessageWithReturnWidget = Widget Function( ReplyMessage? replyMessage, + FocusNode? focusNode, ); typedef ReplyMessageCallBack = void Function(ReplyMessage replyMessage); typedef VoidCallBack = void Function(); diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index 433a09f9..d924c756 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -300,8 +300,9 @@ class _ChatBubbleWidgetState extends State { if (replyMessage.isNotEmpty) chatListConfig.repliedMessageConfig?.repliedMessageWidgetBuilder != null - ? chatListConfig.repliedMessageConfig! - .repliedMessageWidgetBuilder!(widget.message.replyMessage) + ? chatListConfig + .repliedMessageConfig!.repliedMessageWidgetBuilder!( + widget.message.replyMessage, null) : ReplyMessageWidget( message: widget.message, repliedMessageConfig: chatListConfig.repliedMessageConfig, diff --git a/lib/src/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index 10c880f3..dfeb99fd 100644 --- a/lib/src/widgets/send_message_widget.dart +++ b/lib/src/widgets/send_message_widget.dart @@ -249,7 +249,8 @@ class SendMessageWidgetState extends State { valueListenable: _replyMessage, ), widget.sendMessageBuilder != null - ? widget.sendMessageBuilder!(replyMessage) + ? widget.sendMessageBuilder!( + replyMessage, _focusNode) : ChatUITextField( focusNode: _focusNode, textEditingController: _textEditingController, From f52f4fb55448b622d50e7ceae1f6ee263d106485 Mon Sep 17 00:00:00 2001 From: bmlee Date: Sun, 2 Feb 2025 21:32:56 +0900 Subject: [PATCH 13/26] =?UTF-8?q?imageMessage=20margin=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/image_message_view.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/src/widgets/image_message_view.dart b/lib/src/widgets/image_message_view.dart index 08e9a307..b680243b 100644 --- a/lib/src/widgets/image_message_view.dart +++ b/lib/src/widgets/image_message_view.dart @@ -95,10 +95,7 @@ class ImageMessageView extends StatelessWidget { padding: imageMessageConfig?.padding ?? EdgeInsets.zero, margin: imageMessageConfig?.margin ?? EdgeInsets.only( - top: 6, - right: isMessageBySender ? 6 : 0, - left: isMessageBySender ? 0 : 6, - bottom: message.reaction.reactions.isNotEmpty ? 15 : 0, + left: isMessageBySender ? 0 : 12, ), height: imageMessageConfig?.height ?? 200, width: imageMessageConfig?.width ?? 150, From 17d90da590fcaf60bd104ab2ae50584fbea8331e Mon Sep 17 00:00:00 2001 From: Jaeyong Kwack Date: Tue, 4 Feb 2025 00:59:34 +0900 Subject: [PATCH 14/26] =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EB=9E=9C=EB=8D=94=EB=A7=81=20=ED=8D=BC=ED=8F=AC=EB=A8=BC?= =?UTF-8?q?=EC=8A=A4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/chat_view.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/chat_view.dart b/lib/src/widgets/chat_view.dart index bde26517..3e48b4d9 100644 --- a/lib/src/widgets/chat_view.dart +++ b/lib/src/widgets/chat_view.dart @@ -190,11 +190,12 @@ class _ChatViewState extends State @override Widget build(BuildContext context) { - // Scroll to last message on in hasMessages state. + // 채팅 내역이 있을 경우 타이핑 지시자에 따른 마지막 메시지로 스크롤 if (widget.chatController.showTypingIndicator && chatViewState.hasMessages) { chatController.scrollToLastMessage(); } + return ChatViewInheritedWidget( chatController: chatController, featureActiveConfig: featureActiveConfig, @@ -240,13 +241,16 @@ class _ChatViewState extends State Expanded( child: Stack( children: [ - if (chatViewState.isLoading) + // 메시지가 없는 경우에만 전체 화면 상태 위젯을 노출 + if (!chatViewState.hasMessages && + chatViewState.isLoading) ChatViewStateWidget( chatViewStateWidgetConfig: chatViewStateConfig?.loadingWidgetConfig, chatViewState: chatViewState, ) - else if (chatViewState.noMessages) + else if (!chatViewState.hasMessages && + chatViewState.noMessages) ChatViewStateWidget( chatViewStateWidgetConfig: chatViewStateConfig?.noMessageWidgetConfig, @@ -254,7 +258,8 @@ class _ChatViewState extends State onReloadButtonTap: chatViewStateConfig?.onReloadButtonTap, ) - else if (chatViewState.isError) + else if (!chatViewState.hasMessages && + chatViewState.isError) ChatViewStateWidget( chatViewStateWidgetConfig: chatViewStateConfig?.errorWidgetConfig, @@ -262,7 +267,8 @@ class _ChatViewState extends State onReloadButtonTap: chatViewStateConfig?.onReloadButtonTap, ) - else if (chatViewState.hasMessages) + else + // 메시지가 존재하는 경우 채팅 리스트를 표시 ValueListenableBuilder( valueListenable: replyMessage, builder: (_, state, child) { From 9ba56a1cd87c0c2f4dae2724304fa4940ee911f1 Mon Sep 17 00:00:00 2001 From: bmlee Date: Tue, 4 Feb 2025 22:27:40 +0900 Subject: [PATCH 15/26] =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=20margin=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/chat_bubble_widget.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index d924c756..3a47cc37 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -253,7 +253,6 @@ class _ChatBubbleWidgetState extends State { Padding( padding: const EdgeInsets.only( bottom: 6, - left: 12, ), child: Row( children: [ From aa2d5c30baf4b5fa212eea0ca90ad55444d3886d Mon Sep 17 00:00:00 2001 From: bmlee Date: Tue, 4 Feb 2025 22:32:19 +0900 Subject: [PATCH 16/26] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=84=B8=EB=A1=9C=ED=8F=AD=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=EC=98=B5=EC=85=98=20cover=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/image_message_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/image_message_view.dart b/lib/src/widgets/image_message_view.dart index b680243b..8274086e 100644 --- a/lib/src/widgets/image_message_view.dart +++ b/lib/src/widgets/image_message_view.dart @@ -106,7 +106,7 @@ class ImageMessageView extends StatelessWidget { if (imageUrl.isUrl) { return Image.network( imageUrl, - fit: BoxFit.fitHeight, + fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( From 050f1f82077f2c89af2e04bbb09d62d82339b2af Mon Sep 17 00:00:00 2001 From: bmlee Date: Tue, 4 Feb 2025 22:37:04 +0900 Subject: [PATCH 17/26] =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EC=97=90=EB=8F=84=20=EB=A6=AC=EC=95=A1?= =?UTF-8?q?=EC=85=98=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/message_view.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/message_view.dart b/lib/src/widgets/message_view.dart index 70cc65da..216c317d 100644 --- a/lib/src/widgets/message_view.dart +++ b/lib/src/widgets/message_view.dart @@ -237,7 +237,21 @@ class _MessageViewState extends State ); } else if (widget.message.messageType.isCustom && messageConfig?.customMessageBuilder != null) { - return messageConfig?.customMessageBuilder!(widget.message); + return Stack( + clipBehavior: Clip.none, + children: [ + messageConfig!.customMessageBuilder!(widget.message), + if (widget.message.reaction.reactions.isNotEmpty) + ReactionWidget( + reaction: widget.message.reaction, + messageReactionConfig: + messageConfig?.messageReactionConfig, + isMessageBySender: widget.isMessageBySender, + isMyReaction: widget.message.reaction.reactedUserIds + .contains(widget.controller?.currentUser.id), + ), + ], + ); } }()) ?? const SizedBox(), From a9e1593e02afbf41ac35eac05ec7b65769d861ce Mon Sep 17 00:00:00 2001 From: bmlee Date: Wed, 5 Feb 2025 16:44:40 +0900 Subject: [PATCH 18/26] =?UTF-8?q?webUrl=20=EC=B2=B4=ED=81=AC=20=EC=A0=95?= =?UTF-8?q?=EA=B7=9C=EC=8B=9D=20=EC=B6=94=EA=B0=80=20&=20meta=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=97=86=EB=8A=94=20url=EC=9D=80=20=ED=8C=8C?= =?UTF-8?q?=EB=9E=80=20=EB=A7=81=ED=81=AC=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/extensions/extensions.dart | 30 ++++++++++++++++++++ lib/src/widgets/link_preview.dart | 38 ++++++++++++++++++++++++-- lib/src/widgets/text_message_view.dart | 6 +++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/lib/src/extensions/extensions.dart b/lib/src/extensions/extensions.dart index b7a75236..b4ff0516 100644 --- a/lib/src/extensions/extensions.dart +++ b/lib/src/extensions/extensions.dart @@ -83,6 +83,36 @@ extension ValidateString on String { bool get isUrl => Uri.tryParse(this)?.isAbsolute ?? false; + bool get isWebUrl { + final cleanText = trim(); + + try { + // 1. 기본적인 URL 패턴 체크 + final urlPattern = RegExp( + r'^(https?:\/\/)?' // http:// 또는 https:// (선택적) + r'[-a-zA-Z0-9@:%._\+~#=]{1,256}\.' // 도메인 이름 + r'[a-zA-Z0-9()]{1,6}' // TLD + r'([-a-zA-Z0-9()@:%_\+.~#?&//=]*)', // 경로 및 쿼리 파라미터 + caseSensitive: false, + ); + + if (!urlPattern.hasMatch(cleanText)) return false; + + return true; + } catch (e) { + return false; + } + } + + String get normalizedUrl { + final cleanText = trim(); + if (cleanText.toLowerCase().startsWith('http://') || + cleanText.toLowerCase().startsWith('https://')) { + return cleanText; + } + return 'https://$cleanText'; + } + Widget getUserProfilePicture({ required ChatUser? Function(String) getChatUser, double? profileCircleRadius, diff --git a/lib/src/widgets/link_preview.dart b/lib/src/widgets/link_preview.dart index d44f9e4c..59c6eceb 100644 --- a/lib/src/widgets/link_preview.dart +++ b/lib/src/widgets/link_preview.dart @@ -24,6 +24,7 @@ import 'package:chatview/src/extensions/extensions.dart'; import 'package:chatview/src/models/config_models/link_preview_configuration.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:chatview/chatview.dart'; import '../utils/constants/constants.dart'; @@ -32,6 +33,8 @@ class LinkPreview extends StatelessWidget { Key? key, required this.url, this.linkPreviewConfig, + required this.isMessageBySender, + this.chatBubbleConfig, }) : super(key: key); /// Provides url which is passed in message. @@ -41,6 +44,12 @@ class LinkPreview extends StatelessWidget { /// in message. final LinkPreviewConfiguration? linkPreviewConfig; + /// isMessageBySender + final bool isMessageBySender; + + /// chatBubbleConfig + final ChatBubble? chatBubbleConfig; + @override Widget build(BuildContext context) { return Padding( @@ -52,10 +61,35 @@ class LinkPreview extends StatelessWidget { if (!url.isImageUrl && !(context.chatBubbleConfig?.disableLinkPreview ?? false)) ...{ AnyLinkPreview( - link: url, + link: url.normalizedUrl, removeElevation: true, errorBody: linkPreviewConfig?.errorBody, proxyUrl: linkPreviewConfig?.proxyUrl, + errorWidget: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: isMessageBySender + ? chatBubbleConfig?.color ?? Colors.purple + : chatBubbleConfig?.color ?? Colors.grey.shade500, + borderRadius: chatBubbleConfig?.borderRadius ?? + BorderRadius.circular(12), + ), + child: InkWell( + onTap: _onLinkTap, + child: Text( + url, + style: TextStyle( + color: Colors.blue, + fontSize: 15, + decoration: TextDecoration.underline, + decorationColor: Colors.blue, + fontWeight: FontWeight.w500, + height: 1.5, + ), + ), + ), + ), onTap: _onLinkTap, placeholderWidget: SizedBox( height: MediaQuery.of(context).size.height * 0.25, @@ -69,7 +103,7 @@ class LinkPreview extends StatelessWidget { ), backgroundColor: linkPreviewConfig?.backgroundColor ?? Colors.grey.shade200, - borderRadius: linkPreviewConfig?.borderRadius, + borderRadius: linkPreviewConfig?.borderRadius ?? 12, bodyStyle: linkPreviewConfig?.bodyStyle ?? const TextStyle(color: Colors.black), titleStyle: linkPreviewConfig?.titleStyle, diff --git a/lib/src/widgets/text_message_view.dart b/lib/src/widgets/text_message_view.dart index 7f653d6b..8202670b 100644 --- a/lib/src/widgets/text_message_view.dart +++ b/lib/src/widgets/text_message_view.dart @@ -77,7 +77,7 @@ class TextMessageView extends StatelessWidget { return Stack( clipBehavior: Clip.none, children: [ - if (textMessage.isUrl) + if (textMessage.isWebUrl == true) Container( constraints: BoxConstraints( maxWidth: chatBubbleMaxWidth ?? @@ -85,6 +85,10 @@ class TextMessageView extends StatelessWidget { child: LinkPreview( linkPreviewConfig: _linkPreviewConfig, url: textMessage, + isMessageBySender: isMessageBySender, + chatBubbleConfig: isMessageBySender + ? outgoingChatBubbleConfig + : inComingChatBubbleConfig, ), ) else From e449d93440889be5939854128386eee7da41171b Mon Sep 17 00:00:00 2001 From: bmlee Date: Wed, 5 Feb 2025 17:02:55 +0900 Subject: [PATCH 19/26] =?UTF-8?q?Revert=20"webUrl=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EC=A0=95=EA=B7=9C=EC=8B=9D=20=EC=B6=94=EA=B0=80=20&=20meta?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=97=86=EB=8A=94=20url=EC=9D=80=20?= =?UTF-8?q?=ED=8C=8C=EB=9E=80=20=EB=A7=81=ED=81=AC=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=ED=91=9C=EC=8B=9C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit a9e1593e02afbf41ac35eac05ec7b65769d861ce. --- lib/src/extensions/extensions.dart | 30 -------------------- lib/src/widgets/link_preview.dart | 38 ++------------------------ lib/src/widgets/text_message_view.dart | 6 +--- 3 files changed, 3 insertions(+), 71 deletions(-) diff --git a/lib/src/extensions/extensions.dart b/lib/src/extensions/extensions.dart index b4ff0516..b7a75236 100644 --- a/lib/src/extensions/extensions.dart +++ b/lib/src/extensions/extensions.dart @@ -83,36 +83,6 @@ extension ValidateString on String { bool get isUrl => Uri.tryParse(this)?.isAbsolute ?? false; - bool get isWebUrl { - final cleanText = trim(); - - try { - // 1. 기본적인 URL 패턴 체크 - final urlPattern = RegExp( - r'^(https?:\/\/)?' // http:// 또는 https:// (선택적) - r'[-a-zA-Z0-9@:%._\+~#=]{1,256}\.' // 도메인 이름 - r'[a-zA-Z0-9()]{1,6}' // TLD - r'([-a-zA-Z0-9()@:%_\+.~#?&//=]*)', // 경로 및 쿼리 파라미터 - caseSensitive: false, - ); - - if (!urlPattern.hasMatch(cleanText)) return false; - - return true; - } catch (e) { - return false; - } - } - - String get normalizedUrl { - final cleanText = trim(); - if (cleanText.toLowerCase().startsWith('http://') || - cleanText.toLowerCase().startsWith('https://')) { - return cleanText; - } - return 'https://$cleanText'; - } - Widget getUserProfilePicture({ required ChatUser? Function(String) getChatUser, double? profileCircleRadius, diff --git a/lib/src/widgets/link_preview.dart b/lib/src/widgets/link_preview.dart index 59c6eceb..d44f9e4c 100644 --- a/lib/src/widgets/link_preview.dart +++ b/lib/src/widgets/link_preview.dart @@ -24,7 +24,6 @@ import 'package:chatview/src/extensions/extensions.dart'; import 'package:chatview/src/models/config_models/link_preview_configuration.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:chatview/chatview.dart'; import '../utils/constants/constants.dart'; @@ -33,8 +32,6 @@ class LinkPreview extends StatelessWidget { Key? key, required this.url, this.linkPreviewConfig, - required this.isMessageBySender, - this.chatBubbleConfig, }) : super(key: key); /// Provides url which is passed in message. @@ -44,12 +41,6 @@ class LinkPreview extends StatelessWidget { /// in message. final LinkPreviewConfiguration? linkPreviewConfig; - /// isMessageBySender - final bool isMessageBySender; - - /// chatBubbleConfig - final ChatBubble? chatBubbleConfig; - @override Widget build(BuildContext context) { return Padding( @@ -61,35 +52,10 @@ class LinkPreview extends StatelessWidget { if (!url.isImageUrl && !(context.chatBubbleConfig?.disableLinkPreview ?? false)) ...{ AnyLinkPreview( - link: url.normalizedUrl, + link: url, removeElevation: true, errorBody: linkPreviewConfig?.errorBody, proxyUrl: linkPreviewConfig?.proxyUrl, - errorWidget: Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: isMessageBySender - ? chatBubbleConfig?.color ?? Colors.purple - : chatBubbleConfig?.color ?? Colors.grey.shade500, - borderRadius: chatBubbleConfig?.borderRadius ?? - BorderRadius.circular(12), - ), - child: InkWell( - onTap: _onLinkTap, - child: Text( - url, - style: TextStyle( - color: Colors.blue, - fontSize: 15, - decoration: TextDecoration.underline, - decorationColor: Colors.blue, - fontWeight: FontWeight.w500, - height: 1.5, - ), - ), - ), - ), onTap: _onLinkTap, placeholderWidget: SizedBox( height: MediaQuery.of(context).size.height * 0.25, @@ -103,7 +69,7 @@ class LinkPreview extends StatelessWidget { ), backgroundColor: linkPreviewConfig?.backgroundColor ?? Colors.grey.shade200, - borderRadius: linkPreviewConfig?.borderRadius ?? 12, + borderRadius: linkPreviewConfig?.borderRadius, bodyStyle: linkPreviewConfig?.bodyStyle ?? const TextStyle(color: Colors.black), titleStyle: linkPreviewConfig?.titleStyle, diff --git a/lib/src/widgets/text_message_view.dart b/lib/src/widgets/text_message_view.dart index 8202670b..7f653d6b 100644 --- a/lib/src/widgets/text_message_view.dart +++ b/lib/src/widgets/text_message_view.dart @@ -77,7 +77,7 @@ class TextMessageView extends StatelessWidget { return Stack( clipBehavior: Clip.none, children: [ - if (textMessage.isWebUrl == true) + if (textMessage.isUrl) Container( constraints: BoxConstraints( maxWidth: chatBubbleMaxWidth ?? @@ -85,10 +85,6 @@ class TextMessageView extends StatelessWidget { child: LinkPreview( linkPreviewConfig: _linkPreviewConfig, url: textMessage, - isMessageBySender: isMessageBySender, - chatBubbleConfig: isMessageBySender - ? outgoingChatBubbleConfig - : inComingChatBubbleConfig, ), ) else From 758823a2c311ff6370bbb0127cf33d215e5781b1 Mon Sep 17 00:00:00 2001 From: Jaeyong Kwack Date: Fri, 7 Feb 2025 16:38:48 +0900 Subject: [PATCH 20/26] =?UTF-8?q?unreadCount=20value=20notifier=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91=20=EB=B0=8F=20UI=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/controller/chat_controller.dart | 14 +++++ lib/src/models/data_models/message.dart | 10 +++- lib/src/widgets/chat_bubble_widget.dart | 20 +++++--- .../multi_value_listenable_builder.dart | 51 +++++++++++++++++++ 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 lib/src/widgets/multi_value_listenable_builder.dart diff --git a/lib/src/controller/chat_controller.dart b/lib/src/controller/chat_controller.dart index 1f40e2e2..7800129f 100644 --- a/lib/src/controller/chat_controller.dart +++ b/lib/src/controller/chat_controller.dart @@ -100,6 +100,20 @@ class ChatController { } } + void addMessages(List messages) { + initialMessageList.addAll(messages); + if (!messageStreamController.isClosed) { + messageStreamController.sink.add(initialMessageList); + } + } + + void insertPreviousMessages(List messages) { + initialMessageList.insertAll(0, messages); + if (!messageStreamController.isClosed) { + messageStreamController.sink.add(initialMessageList); + } + } + /// Used to add reply suggestions. void addReplySuggestions(List suggestions) { _replySuggestion.value = suggestions; diff --git a/lib/src/models/data_models/message.dart b/lib/src/models/data_models/message.dart index 5be12352..2c6efbef 100644 --- a/lib/src/models/data_models/message.dart +++ b/lib/src/models/data_models/message.dart @@ -55,7 +55,7 @@ class Message { Duration? voiceMessageDuration; // Provides unread count - int? unreadCount; + final ValueNotifier _unreadCount; // Provice custom data Map? customData; @@ -98,6 +98,14 @@ class Message { _status.value = messageStatus; } + int? get unreadCount => _unreadCount.value; + + ValueNotifier get unreadCountNotifier => _unreadCount; + + set setUnreadCount(int? unreadCount) { + _unreadCount.value = unreadCount; + } + factory Message.fromJson(Map json) => Message( id: json['id']?.toString() ?? '', message: json['message']?.toString() ?? '', diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index 3a47cc37..450106e3 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -22,6 +22,7 @@ import 'package:chatview/src/extensions/extensions.dart'; import 'package:chatview/src/utils/constants/constants.dart'; import 'package:chatview/src/widgets/chat_view_inherited_widget.dart'; +import 'package:chatview/src/widgets/multi_value_listenable_builder.dart'; import 'package:flutter/material.dart'; import '../../chatview.dart'; @@ -202,9 +203,12 @@ class _ChatBubbleWidgetState extends State { chatBubbleConfig?.receiptsWidgetConfig?.showReceiptsIn ?? ShowReceiptsIn.lastMessage; if (showReceipts == ShowReceiptsIn.all) { - return ValueListenableBuilder( - valueListenable: widget.message.statusNotifier, - builder: (context, value, child) { + return MultiValueListenableBuilder( + listenables: [ + widget.message.statusNotifier, + widget.message.unreadCountNotifier, + ], + builder: (context, values, child) { if (ChatViewInheritedWidget.of(context) ?.featureActiveConfig .receiptsBuilderVisibility ?? @@ -217,10 +221,12 @@ class _ChatBubbleWidgetState extends State { }, ); } else if (showReceipts == ShowReceiptsIn.lastMessage && isLastMessage) { - return ValueListenableBuilder( - valueListenable: - chatController!.initialMessageList.last.statusNotifier, - builder: (context, value, child) { + return MultiValueListenableBuilder( + listenables: [ + chatController!.initialMessageList.last.statusNotifier, + chatController!.initialMessageList.last.unreadCountNotifier, + ], + builder: (context, values, child) { if (ChatViewInheritedWidget.of(context) ?.featureActiveConfig .receiptsBuilderVisibility ?? diff --git a/lib/src/widgets/multi_value_listenable_builder.dart b/lib/src/widgets/multi_value_listenable_builder.dart new file mode 100644 index 00000000..ca911d56 --- /dev/null +++ b/lib/src/widgets/multi_value_listenable_builder.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +/// 여러 ValueListenable을 동시에 구독하고, +/// 각 listenable의 current value들을 리스트로 전달하는 커스텀 위젯. +class MultiValueListenableBuilder extends StatefulWidget { + final List> listenables; + final Widget Function( + BuildContext context, List values, Widget? child) builder; + final Widget? child; + + const MultiValueListenableBuilder({ + Key? key, + required this.listenables, + required this.builder, + this.child, + }) : super(key: key); + + @override + _MultiValueListenableBuilderState createState() => + _MultiValueListenableBuilderState(); +} + +class _MultiValueListenableBuilderState + extends State { + void _listener() { + setState(() {}); + } + + @override + void initState() { + super.initState(); + for (final listenable in widget.listenables) { + listenable.addListener(_listener); + } + } + + @override + void dispose() { + for (final listenable in widget.listenables) { + listenable.removeListener(_listener); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final values = widget.listenables.map((l) => l.value).toList(); + return widget.builder(context, values, widget.child); + } +} From 85e456de3f95a74c0950529551bc4b10a37d80a0 Mon Sep 17 00:00:00 2001 From: Jaeyong Kwack Date: Sun, 9 Feb 2025 10:19:26 +0900 Subject: [PATCH 21/26] =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/controller/chat_controller.dart | 90 +++++++++++++++++++++++-- lib/src/models/data_models/message.dart | 25 +++++-- lib/src/widgets/chat_bubble_widget.dart | 2 + 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/lib/src/controller/chat_controller.dart b/lib/src/controller/chat_controller.dart index 7800129f..83e40b7a 100644 --- a/lib/src/controller/chat_controller.dart +++ b/lib/src/controller/chat_controller.dart @@ -24,9 +24,19 @@ import 'dart:async'; import 'package:chatview/src/widgets/suggestions/suggestion_list.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:chatview/src/values/enumeration.dart'; import '../models/models.dart'; +/// 메시지 업데이트를 위한 데이터 클래스 +/// id별로 업데이트할 상태(status)와 (nullable) createdAt 값을 포함합니다. +class MessageUpdate { + final MessageStatus status; + final DateTime? createdAt; + + MessageUpdate({required this.status, this.createdAt}); +} + class ChatController { /// Represents initial message list in chat which can be add by user. List initialMessageList; @@ -107,13 +117,6 @@ class ChatController { } } - void insertPreviousMessages(List messages) { - initialMessageList.insertAll(0, messages); - if (!messageStreamController.isClosed) { - messageStreamController.sink.add(initialMessageList); - } - } - /// Used to add reply suggestions. void addReplySuggestions(List suggestions) { _replySuggestion.value = suggestions; @@ -131,6 +134,41 @@ class ChatController { } } + void deleteMessage(String messageId) { + initialMessageList.removeWhere((element) => element.id == messageId); + if (!messageStreamController.isClosed) { + messageStreamController.sink.add(initialMessageList); + } + } + + /// 여러 메시지의 상태를 동시에 업데이트합니다. + /// + /// [updates] 는 key가 메시지 ID(String), + /// 값이 해당 메시지의 업데이트 정보인 [MessageUpdate]인 Map입니다. + /// + /// 내부적으로 리스트를 역순으로 순회하여, 해당 메시지가 존재하면 업데이트 후에 map에서 제거하며 + /// map이 비면 loop를 즉시 종료하여 성능 최적화를 도모합니다. + void updateMessagesStatus(Map updates) { + for (int i = initialMessageList.length - 1; i >= 0; i--) { + final message = initialMessageList[i]; + if (updates.containsKey(message.id)) { + final update = updates[message.id]!; + // 메시지 상태 업데이트 + message.setStatus = update.status; + // createdAt 정보가 있을 경우 업데이트 + if (update.createdAt != null) { + message.setCreatedAt = update.createdAt!; + } + // 해당 업데이트 데이터 제거 + updates.remove(message.id); + } + // 모든 업데이트가 처리되었으면 종료 + if (updates.isEmpty) { + break; + } + } + } + /// Function for setting reaction on specific chat bubble void setReaction({ required String emoji, @@ -194,4 +232,42 @@ class ChatController { ChatUser getUserFromId(String userId) => userId == currentUser.id ? currentUser : otherUsers.firstWhere((element) => element.id == userId); + + /// 주어진 시간 범위 내의 메시지들만 정렬합니다. + /// + /// [from] 부터 [to] 까지 사이에 있는 메시지에 대해, 메시지의 [createdAt] 값을 기준으로 오름차순으로 정렬한 후 + /// 해당 위치에 정렬된 순서로 업데이트합니다. + /// + /// 예를 들어, 순서가 섞여있는 새로운 메시지들이 수신되었을 경우 해당 시간 구간 내에서만 정렬하여 전체 리스트에 반영합니다. + void sortMessagesByCreatedAt(DateTime from, DateTime to) { + // 해당 시간 범위에 포함되는 메시지들의 인덱스와 메시지 객체를 수집합니다. + final List targetIndices = []; + final List targetMessages = []; + + for (int i = 0; i < initialMessageList.length; i++) { + final message = initialMessageList[i]; + // createdAt 값이 null이 아니고, from 초과 to 미만인 경우에 포함시킵니다. + if (message.createdAt != null && + message.createdAt!.isAfter(from) && + message.createdAt!.isBefore(to)) { + targetIndices.add(i); + targetMessages.add(message); + } + } + + if (targetMessages.isEmpty) return; // 정렬 대상 메시지가 없다면 종료 + + // createdAt을 기준으로 오름차순 정렬 (오래된 순으로) + targetMessages.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); + + // 정렬된 메시지별로 원래 리스트의 해당 인덱스 위치를 업데이트합니다. + for (int j = 0; j < targetIndices.length; j++) { + initialMessageList[targetIndices[j]] = targetMessages[j]; + } + + // 리스트 내의 변경사항을 구독자에게 반영하기 위해 업데이트 이벤트를 발행합니다. + if (!messageStreamController.isClosed) { + messageStreamController.sink.add(initialMessageList); + } + } } diff --git a/lib/src/models/data_models/message.dart b/lib/src/models/data_models/message.dart index 2c6efbef..b9de512b 100644 --- a/lib/src/models/data_models/message.dart +++ b/lib/src/models/data_models/message.dart @@ -33,9 +33,6 @@ class Message { /// Provides actual message it will be text or image/audio file path. final String message; - /// Provides message created date time. - final DateTime createdAt; - /// Provides id of sender of message. final String sentBy; @@ -57,13 +54,16 @@ class Message { // Provides unread count final ValueNotifier _unreadCount; + /// Provides message created date time. + final ValueNotifier _createdAt; + // Provice custom data Map? customData; Message({ this.id = '', required this.message, - required this.createdAt, + required DateTime createdAt, required this.sentBy, this.replyMessage = const ReplyMessage(), Reaction? reaction, @@ -71,10 +71,12 @@ class Message { this.voiceMessageDuration, MessageStatus status = MessageStatus.pending, this.customData, - this.unreadCount, + int? unreadCount, }) : reaction = reaction ?? Reaction(reactions: [], reactedUserIds: []), key = GlobalKey(), _status = ValueNotifier(status), + _createdAt = ValueNotifier(createdAt), + _unreadCount = ValueNotifier(unreadCount), assert( (messageType.isVoice ? ((defaultTargetPlatform == TargetPlatform.iOS || @@ -83,6 +85,17 @@ class Message { "Voice messages are only supported with android and ios platform", ); + /// Get current createdAt value + DateTime get createdAt => _createdAt.value; + + /// Get createdAt ValueNotifier + ValueNotifier get createdAtNotifier => _createdAt; + + /// Set new createdAt value + set setCreatedAt(DateTime dateTime) { + _createdAt.value = dateTime; + } + /// curret messageStatus MessageStatus get status => _status.value; @@ -131,7 +144,7 @@ class Message { Map toJson() => { 'id': id, 'message': message, - 'createdAt': createdAt.toIso8601String(), + 'createdAt': _createdAt.value.toIso8601String(), 'sentBy': sentBy, 'reply_message': replyMessage.toJson(), 'reaction': reaction.toJson(), diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index 450106e3..e97c85f6 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -207,6 +207,7 @@ class _ChatBubbleWidgetState extends State { listenables: [ widget.message.statusNotifier, widget.message.unreadCountNotifier, + widget.message.createdAtNotifier, ], builder: (context, values, child) { if (ChatViewInheritedWidget.of(context) @@ -225,6 +226,7 @@ class _ChatBubbleWidgetState extends State { listenables: [ chatController!.initialMessageList.last.statusNotifier, chatController!.initialMessageList.last.unreadCountNotifier, + chatController!.initialMessageList.last.createdAtNotifier, ], builder: (context, values, child) { if (ChatViewInheritedWidget.of(context) From 6fed8edd13cf3c61354a5fa580e7b57c87bc9fec Mon Sep 17 00:00:00 2001 From: bmlee Date: Mon, 10 Feb 2025 13:12:26 +0900 Subject: [PATCH 22/26] =?UTF-8?q?message=20reaction=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=EC=8B=9C=20customData,=20unreadCount=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/controller/chat_controller.dart | 2 ++ lib/src/models/data_models/message.dart | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/lib/src/controller/chat_controller.dart b/lib/src/controller/chat_controller.dart index 83e40b7a..7fb7b1ed 100644 --- a/lib/src/controller/chat_controller.dart +++ b/lib/src/controller/chat_controller.dart @@ -200,6 +200,8 @@ class ChatController { reaction: message.reaction, messageType: message.messageType, status: message.status, + unreadCount: message.unreadCount, + customData: message.customData, ); if (!messageStreamController.isClosed) { messageStreamController.sink.add(initialMessageList); diff --git a/lib/src/models/data_models/message.dart b/lib/src/models/data_models/message.dart index b9de512b..93a84309 100644 --- a/lib/src/models/data_models/message.dart +++ b/lib/src/models/data_models/message.dart @@ -139,6 +139,7 @@ class Message { ), status: MessageStatus.tryParse(json['status']?.toString()) ?? MessageStatus.pending, + unreadCount: json['unreadCount']?.toInt(), ); Map toJson() => { @@ -151,6 +152,7 @@ class Message { 'message_type': messageType.name, 'voice_message_duration': voiceMessageDuration?.inMicroseconds, 'status': status.name, + 'unreadCount': unreadCount, }; Message copyWith({ @@ -165,6 +167,7 @@ class Message { Duration? voiceMessageDuration, MessageStatus? status, bool forceNullValue = false, + int? unreadCount, }) { return Message( id: id ?? this.id, @@ -178,6 +181,7 @@ class Message { reaction: reaction ?? this.reaction, replyMessage: replyMessage ?? this.replyMessage, status: status ?? this.status, + unreadCount: unreadCount ?? this.unreadCount, ); } } From 95d1989ff05c8d38feab7e8f8cf179451df0fa4b Mon Sep 17 00:00:00 2001 From: bmlee Date: Mon, 10 Feb 2025 16:12:54 +0900 Subject: [PATCH 23/26] =?UTF-8?q?chatview=20message=20update=EC=97=90=20cu?= =?UTF-8?q?stomData,=20unreadCount=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/controller/chat_controller.dart | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/src/controller/chat_controller.dart b/lib/src/controller/chat_controller.dart index 7fb7b1ed..f6c5c3da 100644 --- a/lib/src/controller/chat_controller.dart +++ b/lib/src/controller/chat_controller.dart @@ -33,8 +33,15 @@ import '../models/models.dart'; class MessageUpdate { final MessageStatus status; final DateTime? createdAt; - - MessageUpdate({required this.status, this.createdAt}); + final Map? customData; + final int? unreadCount; + + MessageUpdate({ + required this.status, + this.createdAt, + this.customData, + this.unreadCount, + }); } class ChatController { @@ -159,8 +166,18 @@ class ChatController { if (update.createdAt != null) { message.setCreatedAt = update.createdAt!; } + if (update.customData != null) { + message.customData = update.customData; + } + if (update.unreadCount != null) { + message.setUnreadCount = update.unreadCount!; + } // 해당 업데이트 데이터 제거 updates.remove(message.id); + + if (!messageStreamController.isClosed) { + messageStreamController.sink.add(initialMessageList); + } } // 모든 업데이트가 처리되었으면 종료 if (updates.isEmpty) { From d786d662a34584defb13d0141095d90a4a9c9185 Mon Sep 17 00:00:00 2001 From: bmlee Date: Tue, 11 Feb 2025 11:42:06 +0900 Subject: [PATCH 24/26] =?UTF-8?q?=EB=A7=81=ED=81=AC=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/extensions/extensions.dart | 6 ++++ lib/src/widgets/link_preview.dart | 7 +++++ lib/src/widgets/text_message_view.dart | 39 +++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lib/src/extensions/extensions.dart b/lib/src/extensions/extensions.dart index b7a75236..11c2b691 100644 --- a/lib/src/extensions/extensions.dart +++ b/lib/src/extensions/extensions.dart @@ -83,6 +83,12 @@ extension ValidateString on String { bool get isUrl => Uri.tryParse(this)?.isAbsolute ?? false; + bool get isWebUrl { + final pattern = r'^https?:\/\/[-a-zA-Z0-9@:%._\+~#=]+([\/?].*)?$'; + final regExp = RegExp(pattern, caseSensitive: false); + return regExp.hasMatch(this); + } + Widget getUserProfilePicture({ required ChatUser? Function(String) getChatUser, double? profileCircleRadius, diff --git a/lib/src/widgets/link_preview.dart b/lib/src/widgets/link_preview.dart index d44f9e4c..c3348b16 100644 --- a/lib/src/widgets/link_preview.dart +++ b/lib/src/widgets/link_preview.dart @@ -32,6 +32,7 @@ class LinkPreview extends StatelessWidget { Key? key, required this.url, this.linkPreviewConfig, + this.errorWidget, }) : super(key: key); /// Provides url which is passed in message. @@ -41,6 +42,9 @@ class LinkPreview extends StatelessWidget { /// in message. final LinkPreviewConfiguration? linkPreviewConfig; + /// Provides error widget when link/URL is invalid. + final Widget? errorWidget; + @override Widget build(BuildContext context) { return Padding( @@ -55,6 +59,9 @@ class LinkPreview extends StatelessWidget { link: url, removeElevation: true, errorBody: linkPreviewConfig?.errorBody, + errorImage: + 'https://imagedelivery.net/hftuYAvwaYr78lZIcGkPyQ/5705788b-a151-437c-7fcc-f553156ea700/public', + errorWidget: errorWidget, proxyUrl: linkPreviewConfig?.proxyUrl, onTap: _onLinkTap, placeholderWidget: SizedBox( diff --git a/lib/src/widgets/text_message_view.dart b/lib/src/widgets/text_message_view.dart index 7f653d6b..ea3d4ff2 100644 --- a/lib/src/widgets/text_message_view.dart +++ b/lib/src/widgets/text_message_view.dart @@ -21,9 +21,11 @@ */ import 'package:chatview/chatview.dart'; import 'package:flutter/material.dart'; +import 'package:any_link_preview/any_link_preview.dart'; import 'package:chatview/src/extensions/extensions.dart'; import 'package:chatview/src/models/models.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../utils/constants/constants.dart'; import 'link_preview.dart'; @@ -77,7 +79,7 @@ class TextMessageView extends StatelessWidget { return Stack( clipBehavior: Clip.none, children: [ - if (textMessage.isUrl) + if (textMessage.isWebUrl) Container( constraints: BoxConstraints( maxWidth: chatBubbleMaxWidth ?? @@ -85,6 +87,41 @@ class TextMessageView extends StatelessWidget { child: LinkPreview( linkPreviewConfig: _linkPreviewConfig, url: textMessage, + errorWidget: GestureDetector( + onTap: () async { + final uri = Uri.parse(textMessage); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + throw Exception('유효하지 않은 주소입니다.'); + } + }, + child: Container( + constraints: BoxConstraints( + maxWidth: chatBubbleMaxWidth ?? + MediaQuery.of(context).size.width * 0.75), + padding: _padding ?? + const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + margin: _margin ?? + EdgeInsets.fromLTRB(5, 0, 6, + message.reaction.reactions.isNotEmpty ? 15 : 2), + decoration: BoxDecoration( + color: highlightMessage ? highlightColor : _color, + borderRadius: _borderRadius(textMessage), + ), + child: Text( + textMessage, + style: _textStyle?.copyWith( + color: Colors.blue, + decoration: TextDecoration.underline, + decorationColor: Colors.blue, + ), + ), + ), + ), ), ) else From c5bdebc51214008f3bed72ffe2e40a9280fb6877 Mon Sep 17 00:00:00 2001 From: Jaeyong Kwack Date: Tue, 11 Feb 2025 19:31:45 +0900 Subject: [PATCH 25/26] =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9C=84=EC=A0=AF=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=ED=8D=BC=ED=8F=AC=EB=A8=BC=EC=8A=A4=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/chat_groupedlist_widget.dart | 288 ++++++++----------- 1 file changed, 123 insertions(+), 165 deletions(-) diff --git a/lib/src/widgets/chat_groupedlist_widget.dart b/lib/src/widgets/chat_groupedlist_widget.dart index 9ba60f78..61fc08e8 100644 --- a/lib/src/widgets/chat_groupedlist_widget.dart +++ b/lib/src/widgets/chat_groupedlist_widget.dart @@ -144,61 +144,137 @@ class _ChatGroupedListWidgetState extends State Widget build(BuildContext context) { final suggestionsListConfig = suggestionsConfig?.listConfig ?? const SuggestionListConfig(); - return SingleChildScrollView( - reverse: true, - // When reaction popup is being appeared at that user should not scroll. - physics: showPopUp ? const NeverScrollableScrollPhysics() : null, - controller: widget.scrollController, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onHorizontalDragUpdate: (details) => - isEnableSwipeToSeeTime && !showPopUp - ? _onHorizontalDrag(details) - : null, - onHorizontalDragEnd: (details) => - isEnableSwipeToSeeTime && !showPopUp - ? _animationController?.reverse() - : null, - onTap: widget.onChatListTap, - child: _animationController != null - ? AnimatedBuilder( - animation: _animationController!, - builder: (context, child) { - return _chatStreamBuilder; - }, - ) - : _chatStreamBuilder, + return GestureDetector( + onHorizontalDragUpdate: + isEnableSwipeToSeeTime && !showPopUp ? _onHorizontalDrag : null, + onHorizontalDragEnd: isEnableSwipeToSeeTime && !showPopUp + ? (details) => _animationController?.reverse() + : null, + onTap: widget.onChatListTap, + child: CustomScrollView( + reverse: true, + controller: widget.scrollController, + physics: showPopUp ? const NeverScrollableScrollPhysics() : null, + // 화면에 보이지 않는 영역에 대해서도 미리 위젯을 생성하여 스크롤 성능 개선 (필요에 따라 값 조정) + cacheExtent: 1000, + slivers: [ + // 메시지 입력 필드 위쪽 여백 + SliverToBoxAdapter( + child: SizedBox( + height: chatTextFieldHeight, + ), ), + // 메시지 리스트 영역 (직접 SliverList 반환) + _buildMessageSliver(), + // 타이핑 인디케이터 영역 if (chatController != null) - ValueListenableBuilder( - valueListenable: chatController!.typingIndicatorNotifier, - builder: (context, value, child) => TypingIndicator( - typeIndicatorConfig: chatListConfig.typeIndicatorConfig, - chatBubbleConfig: - chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig, - showIndicator: value, + SliverToBoxAdapter( + child: ValueListenableBuilder( + valueListenable: chatController!.typingIndicatorNotifier, + builder: (context, value, child) => TypingIndicator( + typeIndicatorConfig: chatListConfig.typeIndicatorConfig, + chatBubbleConfig: + chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig, + showIndicator: value, + ), ), ), + // SuggestionList 영역 if (chatController != null) - Flexible( + SliverToBoxAdapter( child: Align( alignment: suggestionsListConfig.axisAlignment.alignment, child: const SuggestionList(), ), ), - - // Adds bottom space to the message list, ensuring it is displayed - // above the message text field. - SizedBox( - height: chatTextFieldHeight, - ), ], ), ); } + /// 메시지 리스트를 SliverList로 렌더링하는 메서드 + Widget _buildMessageSliver() { + return StreamBuilder>( + stream: chatController?.messageStreamController.stream, + builder: (context, snapshot) { + if (!snapshot.connectionState.isActive || !snapshot.hasData) { + // 메시지 데이터가 준비되지 않은 경우 전 영역을 채우는 로딩 위젯 반환 + return SliverFillRemaining( + child: Center( + child: chatBackgroundConfig.loadingWidget ?? + const CircularProgressIndicator(), + ), + ); + } else { + final messages = chatBackgroundConfig.sortEnable + ? sortMessage(snapshot.data!) + : snapshot.data!; + final enableSeparator = + featureActiveConfig?.enableChatSeparator ?? false; + + // 메시지 및 그룹 separator를 하나의 리스트로 결합 + List<_ChatListItem> items = []; + if (enableSeparator) { + DateTime? lastDate; + // loop backward in the most performant way + for (var i = messages.length - 1; i >= 0; i--) { + final message = messages[i]; + // 첫 메시지이거나 날짜가 달라졌다면 separator 추가 + if (lastDate == null || + lastDate.getDateFromDateTime != + message.createdAt.getDateFromDateTime) { + if (lastDate != null) { + items.add(_ChatListItem(separator: lastDate)); + } + lastDate = message.createdAt; + } + items.add(_ChatListItem(message: message)); + } + } else { + items = messages.map((m) => _ChatListItem(message: m)).toList(); + } + + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final item = items[index]; + if (item.separator != null) { + return _groupSeparator(item.separator!); + } else { + final message = item.message!; + final enableScrollToRepliedMsg = chatListConfig + .repliedMessageConfig + ?.repliedMsgAutoScrollConfig + .enableScrollToRepliedMsg ?? + false; + return ValueListenableBuilder( + valueListenable: _replyId, + builder: (context, state, child) { + return ChatBubbleWidget( + key: message.key, + message: message, + slideAnimation: _slideAnimation, + onLongPress: (yCoordinate, xCoordinate) => + widget.onChatBubbleLongPress( + yCoordinate, xCoordinate, message), + onSwipe: widget.assignReplyMessage, + shouldHighlight: state == message.id, + onReplyTap: enableScrollToRepliedMsg + ? (replyId) => _onReplyTap(replyId, snapshot.data) + : null, + ); + }, + ); + } + }, + childCount: items.length, + ), + ); + } + }, + ); + } + Future _onReplyTap(String id, List? messages) async { // Finds the replied message if exists final repliedMessages = messages?.firstWhere((message) => id == message.id); @@ -250,95 +326,6 @@ class _ChatGroupedListWidgetState extends State super.dispose(); } - Widget get _chatStreamBuilder { - DateTime lastMatchedDate = DateTime.now(); - return StreamBuilder>( - stream: chatController?.messageStreamController.stream, - builder: (context, snapshot) { - if (!snapshot.connectionState.isActive) { - return Center( - child: chatBackgroundConfig.loadingWidget ?? - const CircularProgressIndicator(), - ); - } else { - final messages = chatBackgroundConfig.sortEnable - ? sortMessage(snapshot.data!) - : snapshot.data!; - - final enableSeparator = - featureActiveConfig?.enableChatSeparator ?? false; - - Map messageSeparator = {}; - - if (enableSeparator) { - /// Get separator when date differ for two messages - (messageSeparator, lastMatchedDate) = _getMessageSeparator( - messages, - lastMatchedDate, - ); - } - - /// [count] that indicates how many separators - /// needs to be display in chat - var count = 0; - - return ListView.builder( - key: widget.key, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: (enableSeparator - ? messages.length + messageSeparator.length - : messages.length), - itemBuilder: (context, index) { - /// By removing [count] from [index] will get actual index - /// to display message in chat - var newIndex = index - count; - - /// Check [messageSeparator] contains group separator for [index] - if (enableSeparator && messageSeparator.containsKey(index)) { - /// Increase counter each time - /// after separating messages with separator - count++; - return _groupSeparator( - messageSeparator[index]!, - ); - } - - return ValueListenableBuilder( - valueListenable: _replyId, - builder: (context, state, child) { - final message = messages[newIndex]; - final enableScrollToRepliedMsg = chatListConfig - .repliedMessageConfig - ?.repliedMsgAutoScrollConfig - .enableScrollToRepliedMsg ?? - false; - return ChatBubbleWidget( - key: message.key, - message: message, - slideAnimation: _slideAnimation, - onLongPress: (yCoordinate, xCoordinate) => - widget.onChatBubbleLongPress( - yCoordinate, - xCoordinate, - message, - ), - onSwipe: widget.assignReplyMessage, - shouldHighlight: state == message.id, - onReplyTap: enableScrollToRepliedMsg - ? (replyId) => _onReplyTap(replyId, snapshot.data) - : null, - ); - }, - ); - }, - ); - } - }, - ); - } - List sortMessage(List messages) { final elements = [...messages]; elements.sort( @@ -378,42 +365,6 @@ class _ChatGroupedListWidgetState extends State ) : const SizedBox.shrink(); } - - GetMessageSeparator _getMessageSeparator( - List messages, - DateTime lastDate, - ) { - final messageSeparator = {}; - var lastMatchedDate = lastDate; - var counter = 0; - - /// Holds index and separator mapping to display in chat - for (var i = 0; i < messages.length; i++) { - if (messageSeparator.isEmpty) { - /// Separator for initial message - messageSeparator[0] = messages[0].createdAt; - continue; - } - lastMatchedDate = _groupBy( - messages[i], - lastMatchedDate, - ); - var previousDate = _groupBy( - messages[i - 1], - lastMatchedDate, - ); - - if (previousDate != lastMatchedDate) { - /// Group separator when previous message and - /// current message time differ - counter++; - - messageSeparator[i + counter] = messages[i].createdAt; - } - } - - return (messageSeparator, lastMatchedDate); - } } class _GroupSeparatorBuilder extends StatelessWidget { @@ -437,3 +388,10 @@ class _GroupSeparatorBuilder extends StatelessWidget { ); } } + +/// 메시지와 구분자(separator)를 담기 위한 간단한 클래스 +class _ChatListItem { + final Message? message; + final DateTime? separator; + _ChatListItem({this.message, this.separator}); +} From 0995ec6a460464e27e4c46f2fc0e1b4b46190217 Mon Sep 17 00:00:00 2001 From: Jaeyong Kwack Date: Tue, 11 Feb 2025 22:53:47 +0900 Subject: [PATCH 26/26] =?UTF-8?q?=ED=82=A4=EB=B3=B4=EB=93=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=ED=8D=BC=ED=8F=AC=EB=A8=BC=EC=8A=A4=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/utils/measure_size.dart | 2 +- lib/src/widgets/chat_view.dart | 4 ++-- lib/src/widgets/link_preview.dart | 2 +- lib/src/widgets/message_view.dart | 2 +- lib/src/widgets/reaction_popup.dart | 2 +- lib/src/widgets/reactions_bottomsheet.dart | 2 +- lib/src/widgets/reply_popup_widget.dart | 2 +- lib/src/widgets/send_message_widget.dart | 4 ++-- lib/src/widgets/text_message_view.dart | 6 +++--- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/src/utils/measure_size.dart b/lib/src/utils/measure_size.dart index 564dfecb..261bdaee 100644 --- a/lib/src/utils/measure_size.dart +++ b/lib/src/utils/measure_size.dart @@ -66,6 +66,6 @@ class _MeasureSizeState extends State { /// Below logic checks that end position of widget greater than or less than /// to device width widget.onSizeChange( - (position.dx + newSize!.width) >= MediaQuery.of(context).size.width); + (position.dx + newSize!.width) >= MediaQuery.sizeOf(context).width); } } diff --git a/lib/src/widgets/chat_view.dart b/lib/src/widgets/chat_view.dart index 3e48b4d9..b1d738de 100644 --- a/lib/src/widgets/chat_view.dart +++ b/lib/src/widgets/chat_view.dart @@ -220,9 +220,9 @@ class _ChatViewState extends State children: [ Container( height: chatBackgroundConfig.height ?? - MediaQuery.of(context).size.height, + MediaQuery.sizeOf(context).height, width: chatBackgroundConfig.width ?? - MediaQuery.of(context).size.width, + MediaQuery.sizeOf(context).width, decoration: BoxDecoration( color: chatBackgroundConfig.backgroundColor ?? Colors.white, image: chatBackgroundConfig.backgroundImage != null diff --git a/lib/src/widgets/link_preview.dart b/lib/src/widgets/link_preview.dart index c3348b16..23bff6d0 100644 --- a/lib/src/widgets/link_preview.dart +++ b/lib/src/widgets/link_preview.dart @@ -65,7 +65,7 @@ class LinkPreview extends StatelessWidget { proxyUrl: linkPreviewConfig?.proxyUrl, onTap: _onLinkTap, placeholderWidget: SizedBox( - height: MediaQuery.of(context).size.height * 0.25, + height: MediaQuery.sizeOf(context).height * 0.25, width: double.infinity, child: Center( child: CircularProgressIndicator( diff --git a/lib/src/widgets/message_view.dart b/lib/src/widgets/message_view.dart index 216c317d..7f91c0d2 100644 --- a/lib/src/widgets/message_view.dart +++ b/lib/src/widgets/message_view.dart @@ -225,7 +225,7 @@ class _MessageViewState extends State ); } else if (widget.message.messageType.isVoice) { return VoiceMessageView( - screenWidth: MediaQuery.of(context).size.width, + screenWidth: MediaQuery.sizeOf(context).width, message: widget.message, config: messageConfig?.voiceMessageConfig, onMaxDuration: widget.onMaxDuration, diff --git a/lib/src/widgets/reaction_popup.dart b/lib/src/widgets/reaction_popup.dart index 4e6841e1..07d96823 100644 --- a/lib/src/widgets/reaction_popup.dart +++ b/lib/src/widgets/reaction_popup.dart @@ -84,7 +84,7 @@ class ReactionPopupState extends State @override Widget build(BuildContext context) { - final deviceWidth = MediaQuery.of(context).size.width; + final deviceWidth = MediaQuery.sizeOf(context).width; final toolTipWidth = deviceWidth > 450 ? 450 : deviceWidth; if (showPopUp) { _animationController.forward(); diff --git a/lib/src/widgets/reactions_bottomsheet.dart b/lib/src/widgets/reactions_bottomsheet.dart index 1ea85eb5..df8da1e5 100644 --- a/lib/src/widgets/reactions_bottomsheet.dart +++ b/lib/src/widgets/reactions_bottomsheet.dart @@ -21,7 +21,7 @@ class ReactionsBottomSheet { context: context, builder: (BuildContext context) { return Container( - height: MediaQuery.of(context).size.height * 0.5, + height: MediaQuery.sizeOf(context).height * 0.5, color: reactionsBottomSheetConfig?.backgroundColor, child: ListView.builder( padding: reactionsBottomSheetConfig?.bottomSheetPadding ?? diff --git a/lib/src/widgets/reply_popup_widget.dart b/lib/src/widgets/reply_popup_widget.dart index f47d28ea..c8e9df89 100644 --- a/lib/src/widgets/reply_popup_widget.dart +++ b/lib/src/widgets/reply_popup_widget.dart @@ -62,7 +62,7 @@ class ReplyPopupWidget extends StatelessWidget { Widget build(BuildContext context) { final textStyle = buttonTextStyle ?? const TextStyle(fontSize: 14, color: Colors.black); - final deviceWidth = MediaQuery.of(context).size.width; + final deviceWidth = MediaQuery.sizeOf(context).width; return Container( height: deviceWidth > 500 ? deviceWidth * 0.05 : deviceWidth * 0.13, decoration: BoxDecoration( diff --git a/lib/src/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index dfeb99fd..490f9f6f 100644 --- a/lib/src/widgets/send_message_widget.dart +++ b/lib/src/widgets/send_message_widget.dart @@ -102,7 +102,7 @@ class SendMessageWidgetState extends State { return Align( alignment: Alignment.bottomCenter, child: SizedBox( - width: MediaQuery.of(context).size.width, + width: MediaQuery.sizeOf(context).width, child: Stack( children: [ // This has been added to prevent messages from being @@ -113,7 +113,7 @@ class SendMessageWidgetState extends State { left: 0, bottom: 0, child: Container( - height: MediaQuery.of(context).size.height / + height: MediaQuery.sizeOf(context).height / ((!kIsWeb && Platform.isIOS) ? 24 : 28), color: chatListConfig.chatBackgroundConfig.backgroundColor ?? Colors.white, diff --git a/lib/src/widgets/text_message_view.dart b/lib/src/widgets/text_message_view.dart index ea3d4ff2..7a242aeb 100644 --- a/lib/src/widgets/text_message_view.dart +++ b/lib/src/widgets/text_message_view.dart @@ -83,7 +83,7 @@ class TextMessageView extends StatelessWidget { Container( constraints: BoxConstraints( maxWidth: chatBubbleMaxWidth ?? - MediaQuery.of(context).size.width * 0.75), + MediaQuery.sizeOf(context).width * 0.75), child: LinkPreview( linkPreviewConfig: _linkPreviewConfig, url: textMessage, @@ -99,7 +99,7 @@ class TextMessageView extends StatelessWidget { child: Container( constraints: BoxConstraints( maxWidth: chatBubbleMaxWidth ?? - MediaQuery.of(context).size.width * 0.75), + MediaQuery.sizeOf(context).width * 0.75), padding: _padding ?? const EdgeInsets.symmetric( horizontal: 12, @@ -128,7 +128,7 @@ class TextMessageView extends StatelessWidget { Container( constraints: BoxConstraints( maxWidth: chatBubbleMaxWidth ?? - MediaQuery.of(context).size.width * 0.75), + MediaQuery.sizeOf(context).width * 0.75), padding: _padding ?? const EdgeInsets.symmetric( horizontal: 12,