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..18c50754 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -853,6 +853,93 @@ 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`** (`oldMessage`) and the **updated `Message`** (`newMessage`). +6. Messages that have been edited display a subtle _Edited_ label below the bubble. + +### Basic Integration + +```dart +ChatView( + // ... + // 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), + // ... +) +``` + +### Enabeling the Feature + +```dart +ChatView( + featureActiveConfig: const FeatureActiveConfig( + enableEditMessage: true, + ), +) +``` + +### 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/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/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..877a5766 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 = false, }); /// 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 `false`. + 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..71242fe6 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 before editing. +/// [updatedMessage] is the updated message object with new content. +typedef EditMessageCallback = void Function( + Message message, + Message 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..b5d71319 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.updatedAt != 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..a8ab9677 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?.currentlyEditingMessage + : null; + } + @override State createState() => _ChatViewState(); } @@ -295,6 +315,17 @@ 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 +345,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..ddfb1426 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 [Message] with new content. + 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? _currentlyEditingMessage; + + /// Whether the text field is currently in edit mode. + bool get isEditMode => _currentlyEditingMessage != null; + + /// Public read-only access to the message currently being edited. + Message? get currentlyEditingMessage => _currentlyEditingMessage; + ReplyMessage get replyMessage => _replyMessage; ChatUser? currentUser; @@ -165,6 +182,19 @@ 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 && + _currentlyEditingMessage != null) { + _textEditingController.clear(); + } + _currentlyEditingMessage = value; + }, + ), if (widget .sendMessageConfig.shouldSendImageWithText) SelectedImageViewWidget( @@ -236,6 +266,19 @@ 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( + _currentlyEditingMessage!, + _currentlyEditingMessage!.copyWith(message: messageText), + ); + _closeEditMode(); + return; + } + if (_selectedImageViewWidgetKey.currentState?.selectedImages.value case final selectedImages?) { for (final image in selectedImages) { @@ -252,10 +295,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(); + } + + _currentlyEditingMessage = 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 (_currentlyEditingMessage == null) return; // Nothing to close. + // Clear the text field when cancelling an edit. + _textEditingController.clear(); + if (_editMessageViewKey.currentState == null) { + setState(() { + _currentlyEditingMessage = 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 +366,8 @@ class SendMessageWidgetState extends State { } else { _replyMessageTextFieldViewKey.currentState?.onClose(); } + // Also clear edit mode if active. + _closeEditMode(); } double get _bottomPadding => (!kIsWeb && Platform.isIOS)