From 0de6001b97e55c272140c3f66d2daa8a35c5db6c Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Sat, 24 Jan 2026 23:25:11 -0300 Subject: [PATCH] feat: add message editing functionality with UI support (#195) * feat: add message editing functionality with UI support * feat: enhance message editing with content length validation and context menu adjustments --- .../conversations/messages_controller.rb | 34 +++++- app/javascript/dashboard/api/inbox/message.js | 7 ++ .../components-next/message/Message.vue | 7 ++ .../components-next/message/MessageList.vue | 6 + .../components-next/message/MessageMeta.vue | 18 +++ .../message/bubbles/Text/FormattedContent.vue | 37 +++++- .../widgets/conversation/MessagesView.vue | 5 + .../i18n/locale/en/conversation.json | 10 ++ .../i18n/locale/pt_BR/conversation.json | 10 ++ .../components/MessageContextMenu.vue | 108 ++++++++++++++++++ .../store/modules/conversations/actions.js | 17 +++ app/models/channel/whatsapp.rb | 7 ++ .../baileys_handlers/messages_update.rb | 6 +- .../providers/whatsapp_baileys_service.rb | 25 +++- .../zapi_handlers/received_callback.rb | 4 +- .../messages/edit_content.json.jbuilder | 1 + config/routes.rb | 1 + 17 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 app/views/api/v1/accounts/conversations/messages/edit_content.json.jbuilder diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb index 67683de86..e9157b876 100644 --- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb @@ -59,6 +59,22 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: render json: { content: translated_content } end + def edit_content + new_content = params[:content] + return render json: { error: 'Content is required' }, status: :unprocessable_entity if new_content.blank? + return render json: { error: 'Content exceeds maximum length' }, status: :unprocessable_entity if new_content.length > 150_000 + return render json: { error: 'Only outgoing messages can be edited' }, status: :forbidden unless message.outgoing? + + original_content = message.content + # Only save previous_content on first edit to preserve the original message + previous_content_to_save = message.is_edited ? message.previous_content : original_content + message.update!(content: new_content, is_edited: true, previous_content: previous_content_to_save) + + edit_message_on_channel(new_content, original_content) + + @message = message.reload + end + private def message @@ -70,7 +86,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: end def permitted_params - params.permit(:id, :target_language, :status, :external_error) + params.permit(:id, :target_language, :status, :external_error, :content) end def already_translated_content_available? @@ -86,6 +102,22 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: Rails.logger.error "Failed to delete message on channel: #{e.message}" end + def edit_message_on_channel(new_content, original_content) + return unless @conversation.inbox.channel.respond_to?(:edit_message) + return if message.source_id.blank? + + @conversation.inbox.channel.edit_message(message, new_content, conversation: @conversation) + rescue StandardError => e + Rails.logger.error "Failed to edit message on channel: #{e.message}" + was_already_edited = message.previous_content != original_content + if was_already_edited + message.update!(content: original_content) + else + message.update!(content: original_content, is_edited: false, previous_content: nil) + end + raise e + end + # API inbox check def ensure_api_inbox # Only API inboxes can update messages diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js index 903e5849c..8f6a98fad 100644 --- a/app/javascript/dashboard/api/inbox/message.js +++ b/app/javascript/dashboard/api/inbox/message.js @@ -92,6 +92,13 @@ class MessageApi extends ApiClient { return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`); } + editContent(conversationID, messageId, content) { + return axios.patch( + `${this.url}/${conversationID}/messages/${messageId}/edit_content`, + { content } + ); + } + retry(conversationID, messageId) { return axios.post( `${this.url}/${conversationID}/messages/${messageId}/retry` diff --git a/app/javascript/dashboard/components-next/message/Message.vue b/app/javascript/dashboard/components-next/message/Message.vue index 6fa7ff2b2..48a660e1e 100644 --- a/app/javascript/dashboard/components-next/message/Message.vue +++ b/app/javascript/dashboard/components-next/message/Message.vue @@ -123,6 +123,7 @@ const props = defineProps({ groupWithNext: { type: Boolean, default: false }, inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties inboxSupportsReplyTo: { type: Object, default: () => ({}) }, + inboxSupportsEdit: { type: Boolean, default: false }, inReplyTo: { type: Object, default: null }, // eslint-disable-line vue/no-unused-properties isEmailInbox: { type: Boolean, default: false }, private: { type: Boolean, default: false }, @@ -371,6 +372,12 @@ const contextMenuEnabledOptions = computed(() => { !props.private && props.inboxSupportsReplyTo.outgoing && !isFailedOrProcessing, + edit: + isOutgoing && + hasText && + !isFailedOrProcessing && + !isMessageDeleted.value && + props.inboxSupportsEdit, }; }); diff --git a/app/javascript/dashboard/components-next/message/MessageList.vue b/app/javascript/dashboard/components-next/message/MessageList.vue index fd725f808..9b198e8d6 100644 --- a/app/javascript/dashboard/components-next/message/MessageList.vue +++ b/app/javascript/dashboard/components-next/message/MessageList.vue @@ -14,6 +14,7 @@ import MessageApi from 'dashboard/api/inbox/message.js'; * @property {Number} currentUserId - ID of the current user * @property {Boolean} isAnEmailChannel - Whether this is an email channel * @property {Object} inboxSupportsReplyTo - Inbox reply support configuration + * @property {Boolean} inboxSupportsEdit - Whether the inbox supports message editing * @property {Array} messages - Array of all messages [These are not in camelcase] */ const props = defineProps({ @@ -33,6 +34,10 @@ const props = defineProps({ type: Object, default: () => ({ incoming: false, outgoing: false }), }, + inboxSupportsEdit: { + type: Boolean, + default: false, + }, messages: { type: Array, default: () => [], @@ -176,6 +181,7 @@ const getInReplyToMessage = parentMessage => { :in-reply-to="getInReplyToMessage(message)" :group-with-next="shouldGroupWithNext(index, allMessages)" :inbox-supports-reply-to="inboxSupportsReplyTo" + :inbox-supports-edit="inboxSupportsEdit" :current-user-id="currentUserId" data-clarity-mask="True" @retry="emit('retry', message)" diff --git a/app/javascript/dashboard/components-next/message/MessageMeta.vue b/app/javascript/dashboard/components-next/message/MessageMeta.vue index 4cc1ffdd7..1e26c3f57 100644 --- a/app/javascript/dashboard/components-next/message/MessageMeta.vue +++ b/app/javascript/dashboard/components-next/message/MessageMeta.vue @@ -126,6 +126,14 @@ const statusToShow = computed(() => { return MESSAGE_STATUS.PROGRESS; }); + +const isEdited = computed(() => { + return contentAttributes.value?.isEdited === true; +}); + +const previousContent = computed(() => { + return contentAttributes.value?.previousContent || ''; +});