From 372f7aa01f5cde813b2cb768560d3ebaab3bbf1b Mon Sep 17 00:00:00 2001 From: Vasu Nageshri Date: Tue, 12 May 2026 10:09:14 +0530 Subject: [PATCH 1/2] feat: Add edit message support. (closes #336) - Add enableEditMessage flag to FeatureActiveConfig (default: true) - Add onEditTap callback to ChatView and ReplyPopupConfiguration - Add EditMessageCallback typedef - Create EditMessageView widget showing 'Editing' indicator above textfield - Extend SendMessageWidget with assignEditMessage() and edit mode logic - Add Edit button to ReplyPopupWidget (own messages only) - Wire assignEditMessage through ChatListWidget -> ChatView - Show 'Edited' label on messages where updateAt is non-null - Add edit/edited/editing strings to ChatViewLocale (backward-compatible) - Expose ChatView.getEditingMessage() static helper - Add doc/documentation.md section for Edit Message feature --- CHANGELOG.md | 1 + doc/documentation.md | 101 ++++++++++++++ lib/chatview.dart | 1 + .../config_models/feature_active_config.dart | 10 ++ .../reply_popup_configuration.dart | 4 + .../send_message_configuration.dart | 10 ++ lib/src/utils/chat_view_locale.dart | 18 +++ lib/src/values/typedefs.dart | 8 ++ lib/src/widgets/chat_bubble_widget.dart | 17 +++ lib/src/widgets/chat_list_widget.dart | 35 +++++ lib/src/widgets/chat_view.dart | 30 ++++ lib/src/widgets/edit_message_view.dart | 132 ++++++++++++++++++ lib/src/widgets/reply_popup_widget.dart | 15 ++ lib/src/widgets/send_message_widget.dart | 82 +++++++++++ 14 files changed, 464 insertions(+) create mode 100644 lib/src/widgets/edit_message_view.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e22beb2..ce6fbe69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased] +* **Feat**: [336](https://github.com/SimformSolutionsPvtLtd/chatview/issues/336) Add edit message support. * **Fix**: [423](https://github.com/SimformSolutionsPvtLtd/chatview/pull/423) Rendering issue in attached image preview when sending message on web. * **Feat**: [420](https://github.com/SimformSolutionsPvtLtd/chatview/pull/420) Added support for diff --git a/doc/documentation.md b/doc/documentation.md index 29f73003..b570cf4c 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -853,6 +853,107 @@ ChatView( lastSeenAgoBuilderVisibility: false, receiptsBuilderVisibility: false, enableTextSelection: true, + enableEditMessage: true, // Enable the Edit Message feature (default: true) + ), + // ... +) +``` + +## Edit Message Feature + +ChatView supports editing previously sent text messages, similar to WhatsApp and Telegram. + +### How It Works + +1. Long-press a message bubble sent by the current user. +2. Tap **Edit** in the reply pop-up. +3. The original text is pre-filled into the input field with an *Editing* indicator above it. +4. Modify the text and press Send. +5. Your `onEditTap` callback is called with the **original `Message`** and the **new text**. +6. Messages that have been edited display a subtle *Edited* label below the bubble. + +### Basic Integration + +```dart +ChatView( + // ... + onEditTap: (Message originalMessage, String newText) { + // Update the message in your data source / backend. + // Set `updateAt` on the message to trigger the "Edited" label in the UI. + final updatedMessage = Message( + id: originalMessage.id, + message: newText, + createdAt: originalMessage.createdAt, + sentBy: originalMessage.sentBy, + updateAt: DateTime.now(), // marks the message as edited + replyMessage: originalMessage.replyMessage, + messageType: originalMessage.messageType, + ); + + chatController.updateMessage(updatedMessage); // depends on your controller API + }, + replyPopupConfig: ReplyPopupConfiguration( + // Optional: react to the edit tap in the popup before the editing UI opens. + onEditTap: (message) => debugPrint('Editing: ${message.id}'), + ), + // ... +) +``` + +### Disabling the Feature + +```dart +ChatView( + featureActiveConfig: const FeatureActiveConfig( + enableEditMessage: false, // hides the Edit button entirely + ), +) +``` + +### Localization + +The edit-related strings can be customized via `ChatViewLocale`: + +```dart +PackageStrings.addLocaleObject( + 'es', + const ChatViewLocale( + // ... all other required fields ... + edit: 'Editar', + edited: 'Editado', + editing: 'Editando', + ), +); +PackageStrings.setLocale('es'); +``` + +### Static Helper + +Use `ChatView.getEditingMessage(context)` to read the message currently being edited +from a widget that is a descendant of `ChatView`: + +```dart +final editingMsg = ChatView.getEditingMessage(context); +if (editingMsg != null) { + // User is in edit mode for editingMsg. +} +``` + +### Customizing the Edit Indicator Label + +When a user enters edit mode, an indicator bar is displayed above the text field showing a label +(default: locale-resolved `"Editing"`). You can override this label via `SendMessageConfiguration`: + +```dart +ChatView( + // ... + sendMessageConfig: SendMessageConfiguration( + /// The label shown in the editing indicator bar above the text field + /// when the user is editing an existing message. + /// + /// If omitted, falls back to the locale-resolved value of + /// `PackageStrings.currentLocale.editing` (e.g. `"Editing"`). + editLabel: 'Editing message', ), // ... ) diff --git a/lib/chatview.dart b/lib/chatview.dart index 5677b8c2..f2a0b241 100644 --- a/lib/chatview.dart +++ b/lib/chatview.dart @@ -57,3 +57,4 @@ export 'src/widgets/action_widgets/text_field_action_button.dart'; export 'src/widgets/chat_list/chatlist.dart'; export 'src/widgets/chat_view.dart'; export 'src/widgets/chat_view_appbar.dart'; +export 'src/widgets/edit_message_view.dart'; diff --git a/lib/src/models/config_models/feature_active_config.dart b/lib/src/models/config_models/feature_active_config.dart index d8ebc145..2b033a34 100644 --- a/lib/src/models/config_models/feature_active_config.dart +++ b/lib/src/models/config_models/feature_active_config.dart @@ -37,6 +37,7 @@ class FeatureActiveConfig { this.enableOtherUserName = true, this.enableScrollToBottomButton = false, this.enableTextSelection = false, + this.enableMessageEditing = true, }); /// Used for enable/disable swipe to reply. @@ -85,4 +86,13 @@ class FeatureActiveConfig { /// /// Defaults to `false`. final bool enableTextSelection; + + /// Used for enable/disable the Edit Message feature. + /// + /// When enabled, an "Edit" option appears in the reply popup for + /// messages sent by the current user, allowing them to modify the + /// message text in-place. + /// + /// Defaults to `true`. + final bool enableMessageEditing; } diff --git a/lib/src/models/config_models/reply_popup_configuration.dart b/lib/src/models/config_models/reply_popup_configuration.dart index e59629f8..86dd4411 100644 --- a/lib/src/models/config_models/reply_popup_configuration.dart +++ b/lib/src/models/config_models/reply_popup_configuration.dart @@ -32,6 +32,7 @@ class ReplyPopupConfiguration { this.onReplyTap, this.onReportTap, this.onMoreTap, + this.onEditTap, this.backgroundColor, this.replyPopupBuilder, }); @@ -48,6 +49,9 @@ class ReplyPopupConfiguration { /// Provides callback on onReply button. final ValueSetter? onReplyTap; + /// Provides callback on edit button (only shown for current user's messages). + final ValueSetter? onEditTap; + /// Provides callback on onReport button. final ValueSetter? onReportTap; diff --git a/lib/src/models/config_models/send_message_configuration.dart b/lib/src/models/config_models/send_message_configuration.dart index bcc586be..baa35554 100644 --- a/lib/src/models/config_models/send_message_configuration.dart +++ b/lib/src/models/config_models/send_message_configuration.dart @@ -53,6 +53,7 @@ class SendMessageConfiguration { this.imageBorderRadius, this.selectedImageViewBuilder, this.sendButtonStyle, + this.editLabel, }); /// Used to give background color to text field. @@ -123,6 +124,15 @@ class SendMessageConfiguration { /// Used to give style to send button. final ButtonStyle? sendButtonStyle; + + /// Custom label displayed in the "editing message" indicator bar shown + /// above the text field when the user is editing an existing message. + /// + /// Example: `"Editing message"` or a localized equivalent. + /// + /// If not provided, falls back to the locale-resolved value of + /// `PackageStrings.currentLocale.editing`. + final String? editLabel; } class ImagePickerIconsConfiguration { diff --git a/lib/src/utils/chat_view_locale.dart b/lib/src/utils/chat_view_locale.dart index 221e7124..6a33249a 100644 --- a/lib/src/utils/chat_view_locale.dart +++ b/lib/src/utils/chat_view_locale.dart @@ -56,6 +56,9 @@ final class ChatViewLocale { required this.deleteChat, required this.noChats, required this.noSearchResults, + this.edit = 'Edit', + this.edited = 'Edited', + this.editing = 'Editing', }); /// Create from `Map` @@ -94,6 +97,9 @@ final class ChatViewLocale { deleteChat: map['deleteChat']?.toString() ?? '', noChats: map['noChats']?.toString() ?? '', noSearchResults: map['noSearchResults']?.toString() ?? '', + edit: map['edit']?.toString() ?? 'Edit', + edited: map['edited']?.toString() ?? 'Edited', + editing: map['editing']?.toString() ?? 'Editing', ); } @@ -131,6 +137,15 @@ final class ChatViewLocale { final String noChats; final String noSearchResults; + /// Label for the edit action button in the reply popup. + final String edit; + + /// Label shown next to an edited message. + final String edited; + + /// Label shown in the text field header when editing a message. + final String editing; + /// English defaults static const en = ChatViewLocale( today: 'Today', @@ -166,5 +181,8 @@ final class ChatViewLocale { deleteChat: 'Delete Chat', noChats: 'No Chats', noSearchResults: 'No search results', + edit: 'Edit', + edited: 'Edited', + editing: 'Editing', ); } diff --git a/lib/src/values/typedefs.dart b/lib/src/values/typedefs.dart index a0ecec67..28133694 100644 --- a/lib/src/values/typedefs.dart +++ b/lib/src/values/typedefs.dart @@ -96,6 +96,14 @@ typedef ChatBubbleLongPressCallback = void Function( double xCordinate, Message message, ); + +/// Callback invoked when the user confirms an edit. +/// [message] is the original message being edited. +/// [updatedMessage] is the updated text content. +typedef EditMessageCallback = void Function( + Message message, + String updatedMessage, +); typedef ChatTextFieldViewBuilderCallback = Widget Function( BuildContext context, T value, diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index a825d43f..bf9b27d2 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -25,6 +25,7 @@ import 'package:flutter/material.dart'; import '../extensions/extensions.dart'; import '../models/config_models/feature_active_config.dart'; import '../utils/constants/constants.dart'; +import '../utils/package_strings.dart'; import '../values/enumeration.dart'; import '../values/typedefs.dart'; import 'chat_view_inherited_widget.dart'; @@ -273,6 +274,22 @@ class _ChatBubbleWidgetState extends State { onTap: () => widget.onReplyTap ?.call(widget.message.replyMessage.messageId), ), + if (widget.message.updateAt != null) + Padding( + padding: EdgeInsets.only( + left: isMessageBySender ? 0 : 8, + right: isMessageBySender ? 8 : 0, + bottom: 2, + ), + child: Text( + PackageStrings.currentLocale.edited, + style: const TextStyle( + fontSize: 11, + color: Colors.grey, + fontStyle: FontStyle.italic, + ), + ), + ), SwipeToReply( isMessageByCurrentUser: isMessageBySender, onSwipe: isMessageBySender ? onLeftSwipe : onRightSwipe, diff --git a/lib/src/widgets/chat_list_widget.dart b/lib/src/widgets/chat_list_widget.dart index 88468994..d1b59438 100644 --- a/lib/src/widgets/chat_list_widget.dart +++ b/lib/src/widgets/chat_list_widget.dart @@ -34,6 +34,7 @@ class ChatListWidget extends StatefulWidget { Key? key, required this.chatController, required this.assignReplyMessage, + this.assignEditMessage, this.loadingWidget, this.loadMoreData, this.isLastPage, @@ -58,6 +59,9 @@ class ChatListWidget extends StatefulWidget { /// bubble. final ValueSetter assignReplyMessage; + /// Provides callback for entering edit mode for a message. + final ValueSetter? assignEditMessage; + /// Provides callback when user tap anywhere on whole chat. final VoidCallback? onChatListTap; @@ -187,12 +191,43 @@ class _ChatListWidgetState extends State { ScaffoldMessenger.of(context).hideCurrentSnackBar(); replyPopup?.onReplyTap?.call(message); }, + onEditTap: _buildEditTapHandler( + message: message, + sentByCurrentUser: sentByCurrentUser, + replyPopup: replyPopup, + ), sentByCurrentUser: sentByCurrentUser, ), ), ).closed; } + VoidCallback? _buildEditTapHandler({ + required Message message, + required bool sentByCurrentUser, + required ReplyPopupConfiguration? replyPopup, + }) { + if (!sentByCurrentUser) return null; + if (!(featureActiveConfig?.enableMessageEditing ?? true)) return null; + + // Only enable Edit when we can actually enter edit mode. + if (widget.assignEditMessage == null) return null; + + return () => _handleEditTap(message, replyPopup); + } + + void _handleEditTap( + Message message, + ReplyPopupConfiguration? replyPopup, + ) { + widget.assignEditMessage?.call(message); + if (featureActiveConfig?.enableReactionPopup ?? false) { + chatViewIW?.showPopUp.value = false; + } + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + replyPopup?.onEditTap?.call(message); + } + void _onChatListTap() { widget.onChatListTap?.call(); if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { diff --git a/lib/src/widgets/chat_view.dart b/lib/src/widgets/chat_view.dart index be3c7a9e..6380f936 100644 --- a/lib/src/widgets/chat_view.dart +++ b/lib/src/widgets/chat_view.dart @@ -43,6 +43,7 @@ class ChatView extends StatefulWidget { required this.chatController, this.typeIndicatorConfig = const TypeIndicatorConfiguration(), this.onSendTap, + this.onEditTap, this.profileCircleConfig, this.chatBubbleConfig, this.repliedMessageConfig, @@ -113,6 +114,10 @@ class ChatView extends StatefulWidget { /// message, reply message and message type. final StringMessageCallBack? onSendTap; + /// Callback invoked when the user confirms an edit on a message. + /// Receives the original [Message] and the updated text. + final EditMessageCallback? onEditTap; + /// Provides builder which helps you to make custom text field and functionality. final ReplyMessageWithReturnWidget? sendMessageBuilder; @@ -174,6 +179,21 @@ class ChatView extends StatefulWidget { return state?._sendMessageKey.currentState?.replyMessage; } + /// Returns the [Message] currently being edited, or `null` if not in edit + /// mode. Useful when you manage the chat state outside of [ChatView]. + static Message? getEditingMessage(BuildContext context) { + final state = context.findAncestorStateOfType<_ChatViewState>(); + + assert( + state != null, + 'ChatViewState not found. Make sure to use correct context that contains the ChatViewState', + ); + + return state?._sendMessageKey.currentState?.isEditMode == true + ? state?._sendMessageKey.currentState?.editMessage + : null; + } + @override State createState() => _ChatViewState(); } @@ -295,6 +315,15 @@ class _ChatViewState extends State assignReplyMessage: (message) => _sendMessageKey.currentState ?.assignReplyMessage(message), + // Only wire assignEditMessage when the built-in + // SendMessageWidget is active. When a custom + // sendMessageBuilder is provided, the caller is + // responsible for handling edit via + // ReplyPopupConfiguration.onEditTap instead. + assignEditMessage: widget.sendMessageBuilder == null + ? (message) => _sendMessageKey.currentState + ?.assignEditMessage(message) + : null, textFieldConfig: widget.sendMessageConfig.textFieldConfig, ), @@ -314,6 +343,7 @@ class _ChatViewState extends State _onSendTap( message, replyMessage, messageType); }, + onEditTap: widget.onEditTap, messageConfig: widget.messageConfig, replyMessageBuilder: widget.replyMessageBuilder, ), diff --git a/lib/src/widgets/edit_message_view.dart b/lib/src/widgets/edit_message_view.dart new file mode 100644 index 00000000..d0823558 --- /dev/null +++ b/lib/src/widgets/edit_message_view.dart @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022 Simform Solutions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import 'package:chatview_utils/chatview_utils.dart'; +import 'package:flutter/material.dart'; + +import '../models/config_models/send_message_configuration.dart'; +import '../utils/constants/constants.dart'; +import '../utils/package_strings.dart'; +import 'chat_textfield_view_builder.dart'; + +/// A widget shown above the text field when the user is editing a message. +/// +/// Displays an "Editing" header and a close button to cancel the edit. +class EditMessageView extends StatefulWidget { + const EditMessageView({ + super.key, + required this.sendMessageConfig, + required this.onChange, + }); + + /// Configuration for the send-message text field area. + final SendMessageConfiguration? sendMessageConfig; + + /// Called whenever the edit-mode message changes (including when cleared). + final ValueSetter onChange; + + @override + State createState() => EditMessageViewState(); +} + +class EditMessageViewState extends State { + /// The message currently being edited. `null` means edit mode is inactive. + final ValueNotifier editMessage = ValueNotifier(null); + + @override + void initState() { + super.initState(); + editMessage.addListener(_handleChange); + } + + @override + Widget build(BuildContext context) { + return ChatTextFieldViewBuilder( + valueListenable: editMessage, + builder: (_, state, __) { + if (state == null) return const SizedBox.shrink(); + + final editLabel = + (widget.sendMessageConfig?.editLabel?.trim().isNotEmpty ?? false) + ? widget.sendMessageConfig!.editLabel!.trim() + : PackageStrings.currentLocale.editing; + final titleColor = widget.sendMessageConfig?.replyTitleColor ?? + Theme.of(context).colorScheme.primary; + + return Container( + decoration: BoxDecoration( + color: widget.sendMessageConfig?.textFieldBackgroundColor ?? + Colors.white, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(14), + ), + ), + padding: const EdgeInsets.only(left: leftPadding, right: leftPadding, bottom: 48), + child: Row( + children: [ + Expanded( + child: Text( + editLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ), + IconButton( + constraints: const BoxConstraints(), + padding: const EdgeInsets.symmetric(horizontal: 8), + icon: Icon( + Icons.close, + color: widget.sendMessageConfig?.closeIconColor ?? + Colors.black54, + size: 18, + ), + onPressed: onClose, + ), + ], + ), + ); + }, + ); + } + + void _handleChange() { + widget.onChange.call(editMessage.value); + } + + /// Clears the edit state, exiting edit mode. + void onClose() { + editMessage.value = null; + } + + @override + void dispose() { + editMessage + ..removeListener(_handleChange) + ..dispose(); + super.dispose(); + } +} diff --git a/lib/src/widgets/reply_popup_widget.dart b/lib/src/widgets/reply_popup_widget.dart index 22db563f..e3d49233 100644 --- a/lib/src/widgets/reply_popup_widget.dart +++ b/lib/src/widgets/reply_popup_widget.dart @@ -31,6 +31,7 @@ class ReplyPopupWidget extends StatelessWidget { required this.onReplyTap, required this.onReportTap, required this.onMoreTap, + this.onEditTap, this.buttonTextStyle, this.topBorderColor, }) : super(key: key); @@ -50,6 +51,9 @@ class ReplyPopupWidget extends StatelessWidget { /// Provides call back when user tap on more button. final VoidCallback onMoreTap; + /// Provides call back when user tap on edit button (own messages only). + final VoidCallback? onEditTap; + /// Allow user to set text style of button are showed in reply snack bar. final TextStyle? buttonTextStyle; @@ -91,6 +95,17 @@ class ReplyPopupWidget extends StatelessWidget { ), ), ), + if (sentByCurrentUser && onEditTap != null) + Expanded( + child: InkWell( + onTap: onEditTap, + child: Text( + PackageStrings.currentLocale.edit, + textAlign: TextAlign.center, + style: textStyle, + ), + ), + ), if (!sentByCurrentUser) Expanded( child: InkWell( diff --git a/lib/src/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index 7d1d0852..070f37cf 100644 --- a/lib/src/widgets/send_message_widget.dart +++ b/lib/src/widgets/send_message_widget.dart @@ -33,12 +33,14 @@ import '../values/typedefs.dart'; import 'chatui_textfield.dart'; import 'reply_message_view.dart'; import 'scroll_to_bottom_button.dart'; +import 'edit_message_view.dart'; import 'selected_image_view_widget.dart'; class SendMessageWidget extends StatefulWidget { const SendMessageWidget({ required this.onSendTap, required this.sendMessageConfig, + this.onEditTap, this.sendMessageBuilder, this.messageConfig, this.replyMessageBuilder, @@ -51,6 +53,10 @@ class SendMessageWidget extends StatefulWidget { /// Provides configuration for text field appearance. final SendMessageConfiguration sendMessageConfig; + /// Callback invoked when the user confirms an edit. + /// Receives the original [Message] and the updated text. + final EditMessageCallback? onEditTap; + /// Allow user to set custom text field. final ReplyMessageWithReturnWidget? sendMessageBuilder; @@ -72,10 +78,21 @@ class SendMessageWidgetState extends State { final GlobalKey _replyMessageTextFieldViewKey = GlobalKey(); + final GlobalKey _editMessageViewKey = GlobalKey(); + final GlobalKey _selectedImageViewWidgetKey = GlobalKey(); ReplyMessage _replyMessage = const ReplyMessage(); + /// The message currently being edited, or `null` if not in edit mode. + Message? _editMessage; + + /// Whether the text field is currently in edit mode. + bool get isEditMode => _editMessage != null; + + /// Public read-only access to the message currently being edited. + Message? get editMessage => _editMessage; + ReplyMessage get replyMessage => _replyMessage; ChatUser? currentUser; @@ -165,6 +182,18 @@ class SendMessageWidgetState extends State { builder: widget.replyMessageBuilder, onChange: (value) => _replyMessage = value, ), + EditMessageView( + key: _editMessageViewKey, + sendMessageConfig: widget.sendMessageConfig, + onChange: (value) { + // When the user cancels via the X button + // on EditMessageView, clear the text field. + if (value == null && _editMessage != null) { + _textEditingController.clear(); + } + _editMessage = value; + }, + ), if (widget .sendMessageConfig.shouldSendImageWithText) SelectedImageViewWidget( @@ -236,6 +265,16 @@ class SendMessageWidgetState extends State { _textEditingController.clear(); if (messageText.isEmpty) return; + // If in edit mode, delegate to onEditTap instead of onSendTap. + if (isEditMode) { + // Only confirm the edit if a handler is registered; otherwise keep edit + // mode active so the user's text is not silently discarded. + if (widget.onEditTap == null) return; + widget.onEditTap!.call(_editMessage!, messageText); + _closeEditMode(); + return; + } + if (_selectedImageViewWidgetKey.currentState?.selectedImages.value case final selectedImages?) { for (final image in selectedImages) { @@ -252,10 +291,51 @@ class SendMessageWidgetState extends State { onCloseTap(); } + /// Puts the text field into edit mode for [message]. + /// Pre-fills the text field with the existing message content. + void assignEditMessage(Message message) { + // Always clear any active reply before entering edit mode. + _replyMessage = const ReplyMessage(); + if (_replyMessageTextFieldViewKey.currentState != null) { + _replyMessageTextFieldViewKey.currentState!.replyMessage.value = + const ReplyMessage(); + } + + _editMessage = message; + _textEditingController.text = message.message; + // Place cursor at end. + _textEditingController.selection = TextSelection.fromPosition( + TextPosition(offset: message.message.length), + ); + FocusScope.of(context).requestFocus(_focusNode); + + if (_editMessageViewKey.currentState == null) { + setState(() {}); + } else { + _editMessageViewKey.currentState!.editMessage.value = message; + } + } + + void _closeEditMode() { + if (_editMessage == null) return; // Nothing to close. + // Clear the text field when cancelling an edit. + _textEditingController.clear(); + if (_editMessageViewKey.currentState == null) { + setState(() { + _editMessage = null; + }); + } else { + _editMessageViewKey.currentState?.onClose(); + } + } + void assignReplyMessage(Message message) { if (currentUser == null) { return; } + // Clear any active edit mode before entering reply mode. + _closeEditMode(); + FocusScope.of(context).requestFocus(_focusNode); _replyMessage = ReplyMessage( message: message.message, @@ -282,6 +362,8 @@ class SendMessageWidgetState extends State { } else { _replyMessageTextFieldViewKey.currentState?.onClose(); } + // Also clear edit mode if active. + _closeEditMode(); } double get _bottomPadding => (!kIsWeb && Platform.isIOS) From 5fcc22f3e6a6231112145c6aa1343cc918974fd2 Mon Sep 17 00:00:00 2001 From: japanshah-simform Date: Tue, 19 May 2026 10:23:03 +0530 Subject: [PATCH 2/2] feat: Enhance edit message functionality with updated callback and integration examples --- doc/documentation.md | 32 ++++++------------- example/lib/main.dart | 3 ++ .../config_models/feature_active_config.dart | 4 +-- lib/src/values/typedefs.dart | 6 ++-- lib/src/widgets/chat_bubble_widget.dart | 2 +- lib/src/widgets/chat_view.dart | 12 ++++--- lib/src/widgets/send_message_widget.dart | 24 ++++++++------ 7 files changed, 39 insertions(+), 44 deletions(-) diff --git a/doc/documentation.md b/doc/documentation.md index b570cf4c..18c50754 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -869,43 +869,29 @@ ChatView supports editing previously sent text messages, similar to WhatsApp and 2. Tap **Edit** in the reply pop-up. 3. The original text is pre-filled into the input field with an *Editing* indicator above it. 4. Modify the text and press Send. -5. Your `onEditTap` callback is called with the **original `Message`** and the **new text**. -6. Messages that have been edited display a subtle *Edited* label below the bubble. +5. Your `onEditTap` callback is called with the **original `Message`** (`oldMessage`) and the **updated `Message`** (`newMessage`). +6. Messages that have been edited display a subtle _Edited_ label below the bubble. ### Basic Integration ```dart ChatView( // ... - onEditTap: (Message originalMessage, String newText) { - // Update the message in your data source / backend. - // Set `updateAt` on the message to trigger the "Edited" label in the UI. - final updatedMessage = Message( - id: originalMessage.id, - message: newText, - createdAt: originalMessage.createdAt, - sentBy: originalMessage.sentBy, - updateAt: DateTime.now(), // marks the message as edited - replyMessage: originalMessage.replyMessage, - messageType: originalMessage.messageType, - ); - - chatController.updateMessage(updatedMessage); // depends on your controller API - }, - replyPopupConfig: ReplyPopupConfiguration( - // Optional: react to the edit tap in the popup before the editing UI opens. - onEditTap: (message) => debugPrint('Editing: ${message.id}'), - ), + // updatedMessage is a copy of oldMessage with the new text already applied. + // You have access to both objects so you can do further transformations — + // for example, update the timestamp, change the message type, or sync with a backend. + onEditTap: (message, updatedMessage) => + _chatController.updateMessage(messageId: message.id, newMessage: updatedMessage), // ... ) ``` -### Disabling the Feature +### Enabeling the Feature ```dart ChatView( featureActiveConfig: const FeatureActiveConfig( - enableEditMessage: false, // hides the Edit button entirely + enableEditMessage: true, ), ) ``` diff --git a/example/lib/main.dart b/example/lib/main.dart index 99fa02a3..455a91d7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -426,6 +426,8 @@ class _ExampleOneChatScreenState extends State { return Scaffold( body: SafeArea( child: ChatView( + onEditTap: (message, updatedMessage) => _chatController.updateMessage( + messageId: message.id, newMessage: updatedMessage), chatController: _chatController, onSendTap: _onSendTap, isLastPage: () => _isTopPaginationCalled && _isBottomPaginationCalled, @@ -495,6 +497,7 @@ class _ExampleOneChatScreenState extends State { enableScrollToBottomButton: true, enableOtherUserProfileAvatar: true, enablePagination: true, + enableMessageEditing: true, ), scrollToBottomButtonConfig: const ScrollToBottomButtonConfig( padding: EdgeInsets.only(bottom: 8, right: 12), diff --git a/lib/src/models/config_models/feature_active_config.dart b/lib/src/models/config_models/feature_active_config.dart index 2b033a34..877a5766 100644 --- a/lib/src/models/config_models/feature_active_config.dart +++ b/lib/src/models/config_models/feature_active_config.dart @@ -37,7 +37,7 @@ class FeatureActiveConfig { this.enableOtherUserName = true, this.enableScrollToBottomButton = false, this.enableTextSelection = false, - this.enableMessageEditing = true, + this.enableMessageEditing = false, }); /// Used for enable/disable swipe to reply. @@ -93,6 +93,6 @@ class FeatureActiveConfig { /// messages sent by the current user, allowing them to modify the /// message text in-place. /// - /// Defaults to `true`. + /// Defaults to `false`. final bool enableMessageEditing; } diff --git a/lib/src/values/typedefs.dart b/lib/src/values/typedefs.dart index 28133694..71242fe6 100644 --- a/lib/src/values/typedefs.dart +++ b/lib/src/values/typedefs.dart @@ -98,11 +98,11 @@ typedef ChatBubbleLongPressCallback = void Function( ); /// Callback invoked when the user confirms an edit. -/// [message] is the original message being edited. -/// [updatedMessage] is the updated text content. +/// [message] is the original message before editing. +/// [updatedMessage] is the updated message object with new content. typedef EditMessageCallback = void Function( Message message, - String updatedMessage, + Message updatedMessage, ); typedef ChatTextFieldViewBuilderCallback = Widget Function( BuildContext context, diff --git a/lib/src/widgets/chat_bubble_widget.dart b/lib/src/widgets/chat_bubble_widget.dart index bf9b27d2..b5d71319 100644 --- a/lib/src/widgets/chat_bubble_widget.dart +++ b/lib/src/widgets/chat_bubble_widget.dart @@ -274,7 +274,7 @@ class _ChatBubbleWidgetState extends State { onTap: () => widget.onReplyTap ?.call(widget.message.replyMessage.messageId), ), - if (widget.message.updateAt != null) + if (widget.message.updatedAt != null) Padding( padding: EdgeInsets.only( left: isMessageBySender ? 0 : 8, diff --git a/lib/src/widgets/chat_view.dart b/lib/src/widgets/chat_view.dart index 6380f936..a8ab9677 100644 --- a/lib/src/widgets/chat_view.dart +++ b/lib/src/widgets/chat_view.dart @@ -190,7 +190,7 @@ class ChatView extends StatefulWidget { ); return state?._sendMessageKey.currentState?.isEditMode == true - ? state?._sendMessageKey.currentState?.editMessage + ? state?._sendMessageKey.currentState?.currentlyEditingMessage : null; } @@ -320,10 +320,12 @@ class _ChatViewState extends State // sendMessageBuilder is provided, the caller is // responsible for handling edit via // ReplyPopupConfiguration.onEditTap instead. - assignEditMessage: widget.sendMessageBuilder == null - ? (message) => _sendMessageKey.currentState - ?.assignEditMessage(message) - : null, + assignEditMessage: + widget.sendMessageBuilder == null + ? (message) => _sendMessageKey + .currentState + ?.assignEditMessage(message) + : null, textFieldConfig: widget.sendMessageConfig.textFieldConfig, ), diff --git a/lib/src/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index 070f37cf..ddfb1426 100644 --- a/lib/src/widgets/send_message_widget.dart +++ b/lib/src/widgets/send_message_widget.dart @@ -54,7 +54,7 @@ class SendMessageWidget extends StatefulWidget { final SendMessageConfiguration sendMessageConfig; /// Callback invoked when the user confirms an edit. - /// Receives the original [Message] and the updated text. + /// Receives the original [Message] and the updated [Message] with new content. final EditMessageCallback? onEditTap; /// Allow user to set custom text field. @@ -85,13 +85,13 @@ class SendMessageWidgetState extends State { ReplyMessage _replyMessage = const ReplyMessage(); /// The message currently being edited, or `null` if not in edit mode. - Message? _editMessage; + Message? _currentlyEditingMessage; /// Whether the text field is currently in edit mode. - bool get isEditMode => _editMessage != null; + bool get isEditMode => _currentlyEditingMessage != null; /// Public read-only access to the message currently being edited. - Message? get editMessage => _editMessage; + Message? get currentlyEditingMessage => _currentlyEditingMessage; ReplyMessage get replyMessage => _replyMessage; @@ -188,10 +188,11 @@ class SendMessageWidgetState extends State { onChange: (value) { // When the user cancels via the X button // on EditMessageView, clear the text field. - if (value == null && _editMessage != null) { + if (value == null && + _currentlyEditingMessage != null) { _textEditingController.clear(); } - _editMessage = value; + _currentlyEditingMessage = value; }, ), if (widget @@ -270,7 +271,10 @@ class SendMessageWidgetState extends State { // Only confirm the edit if a handler is registered; otherwise keep edit // mode active so the user's text is not silently discarded. if (widget.onEditTap == null) return; - widget.onEditTap!.call(_editMessage!, messageText); + widget.onEditTap!.call( + _currentlyEditingMessage!, + _currentlyEditingMessage!.copyWith(message: messageText), + ); _closeEditMode(); return; } @@ -301,7 +305,7 @@ class SendMessageWidgetState extends State { const ReplyMessage(); } - _editMessage = message; + _currentlyEditingMessage = message; _textEditingController.text = message.message; // Place cursor at end. _textEditingController.selection = TextSelection.fromPosition( @@ -317,12 +321,12 @@ class SendMessageWidgetState extends State { } void _closeEditMode() { - if (_editMessage == null) return; // Nothing to close. + if (_currentlyEditingMessage == null) return; // Nothing to close. // Clear the text field when cancelling an edit. _textEditingController.clear(); if (_editMessageViewKey.currentState == null) { setState(() { - _editMessage = null; + _currentlyEditingMessage = null; }); } else { _editMessageViewKey.currentState?.onClose();