Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
87 changes: 87 additions & 0 deletions doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
),
// ...
)
Expand Down
3 changes: 3 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ class _ExampleOneChatScreenState extends State<ExampleOneChatScreen> {
return Scaffold(
body: SafeArea(
child: ChatView(
onEditTap: (message, updatedMessage) => _chatController.updateMessage(
messageId: message.id, newMessage: updatedMessage),
chatController: _chatController,
onSendTap: _onSendTap,
isLastPage: () => _isTopPaginationCalled && _isBottomPaginationCalled,
Expand Down Expand Up @@ -495,6 +497,7 @@ class _ExampleOneChatScreenState extends State<ExampleOneChatScreen> {
enableScrollToBottomButton: true,
enableOtherUserProfileAvatar: true,
enablePagination: true,
enableMessageEditing: true,
),
scrollToBottomButtonConfig: const ScrollToBottomButtonConfig(
padding: EdgeInsets.only(bottom: 8, right: 12),
Expand Down
1 change: 1 addition & 0 deletions lib/chatview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
10 changes: 10 additions & 0 deletions lib/src/models/config_models/feature_active_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
4 changes: 4 additions & 0 deletions lib/src/models/config_models/reply_popup_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ReplyPopupConfiguration {
this.onReplyTap,
this.onReportTap,
this.onMoreTap,
this.onEditTap,
this.backgroundColor,
this.replyPopupBuilder,
});
Expand All @@ -48,6 +49,9 @@ class ReplyPopupConfiguration {
/// Provides callback on onReply button.
final ValueSetter<Message>? onReplyTap;

/// Provides callback on edit button (only shown for current user's messages).
final ValueSetter<Message>? onEditTap;

/// Provides callback on onReport button.
final ValueSetter<Message>? onReportTap;

Expand Down
10 changes: 10 additions & 0 deletions lib/src/models/config_models/send_message_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class SendMessageConfiguration {
this.imageBorderRadius,
this.selectedImageViewBuilder,
this.sendButtonStyle,
this.editLabel,
});

/// Used to give background color to text field.
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions lib/src/utils/chat_view_locale.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>`
Expand Down Expand Up @@ -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',
);
}

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -166,5 +181,8 @@ final class ChatViewLocale {
deleteChat: 'Delete Chat',
noChats: 'No Chats',
noSearchResults: 'No search results',
edit: 'Edit',
edited: 'Edited',
editing: 'Editing',
);
}
8 changes: 8 additions & 0 deletions lib/src/values/typedefs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = Widget Function(
BuildContext context,
T value,
Expand Down
17 changes: 17 additions & 0 deletions lib/src/widgets/chat_bubble_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -273,6 +274,22 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
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,
Expand Down
35 changes: 35 additions & 0 deletions lib/src/widgets/chat_list_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class ChatListWidget extends StatefulWidget {
Key? key,
required this.chatController,
required this.assignReplyMessage,
this.assignEditMessage,
this.loadingWidget,
this.loadMoreData,
this.isLastPage,
Expand All @@ -58,6 +59,9 @@ class ChatListWidget extends StatefulWidget {
/// bubble.
final ValueSetter<Message> assignReplyMessage;

/// Provides callback for entering edit mode for a message.
final ValueSetter<Message>? assignEditMessage;

/// Provides callback when user tap anywhere on whole chat.
final VoidCallback? onChatListTap;

Expand Down Expand Up @@ -187,12 +191,43 @@ class _ChatListWidgetState extends State<ChatListWidget> {
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)) {
Expand Down
Loading
Loading