diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index 2a9577644..1bf08d169 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -313,7 +313,12 @@ const plugins = computed(() => { const sendWithSignature = computed(() => { // this is considered the source of truth, we watch this property // on change, we toggle the signature in the editor - if (props.allowSignature && !props.isPrivate && props.channelType) { + if ( + props.allowSignature && + !props.isPrivate && + props.channelType && + !props.disabled + ) { return fetchSignatureFlagFromUISettings(props.channelType); } @@ -436,6 +441,7 @@ function reloadState(content = props.modelValue) { } function addSignature() { + if (props.disabled) return; let content = props.modelValue; // see if the content is empty, if it is before appending the signature // we need to add a paragraph node and move the cursor at the start of the editor @@ -454,6 +460,7 @@ function addSignature() { } function removeSignature() { + if (props.disabled) return; if (!props.signature) return; let content = props.modelValue; content = removeSignatureHelper( @@ -806,7 +813,7 @@ watch( watch(sendWithSignature, newValue => { // see if the allowSignature flag is true - if (props.allowSignature) { + if (props.allowSignature && !props.disabled) { toggleSignatureInEditor(newValue); } }); diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index 5f76041dc..ff569d763 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -128,7 +128,6 @@ export default { }, }, emits: [ - 'replaceText', 'toggleInsertArticle', 'selectWhatsappTemplate', 'selectContentTemplate', @@ -277,9 +276,6 @@ export default { toggleMessageSignature() { this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature); }, - replaceText(text) { - this.$emit('replaceText', text); - }, toggleInsertArticle() { this.$emit('toggleInsertArticle'); }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index ef6fa03d6..a5122094e 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -27,7 +27,6 @@ import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events'; import { getMessageVariables, getUndefinedVariablesInMessage, - replaceVariablesInMessage, } from '@chatwoot/utils'; import WhatsappTemplates from './WhatsappTemplates/Modal.vue'; import ContentTemplates from './ContentTemplates/ContentTemplatesModal.vue'; @@ -636,10 +635,17 @@ export default { return message; } + // Even when editor is disabled (e.g. WhatsApp/API can't reply), we must + // still normalize stale signatures out of drafts when signature is off. + if (this.isEditorDisabled && this.sendWithSignature) { + return message; + } + const effectiveChannelType = getEffectiveChannelType( this.channelType, this.inbox?.medium || '' ); + return this.sendWithSignature ? appendSignature(message, this.messageSignature, effectiveChannelType) : removeSignature(message, this.messageSignature, effectiveChannelType); @@ -911,32 +917,6 @@ export default { }); this.hideContentTemplatesModal(); }, - replaceText(message) { - if (this.sendWithSignature && !this.private) { - // if signature is enabled, append it to the message - // appendSignature ensures that the signature is not duplicated - // so we don't need to check if the signature is already present - const effectiveChannelType = getEffectiveChannelType( - this.channelType, - this.inbox?.medium || '' - ); - message = appendSignature( - message, - this.messageSignature, - effectiveChannelType - ); - } - - const updatedMessage = replaceVariablesInMessage({ - message, - variables: this.messageVariables, - }); - - setTimeout(() => { - useTrack(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); - this.message = updatedMessage; - }, 100); - }, setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) { // Clear attachments when switching between private note and reply modes // This is to prevent from breaking the upload rules @@ -1435,7 +1415,6 @@ export default { :new-conversation-modal-active="newConversationModalActive" @select-whatsapp-template="openWhatsappTemplateModal" @select-content-template="openContentTemplateModal" - @replace-text="replaceText" @toggle-insert-article="toggleInsertArticle" @toggle-quoted-reply="toggleQuotedReply" /> diff --git a/app/javascript/dashboard/helper/editorHelper.js b/app/javascript/dashboard/helper/editorHelper.js index 25d062650..b3d071ccd 100644 --- a/app/javascript/dashboard/helper/editorHelper.js +++ b/app/javascript/dashboard/helper/editorHelper.js @@ -32,6 +32,25 @@ export function extractTextFromMarkdown(markdown) { .trim(); // Trim any extra space } +/** + * Removes inline base64 markdown images from signature content. + * + * @param {string} content + * @returns {{ sanitizedContent: string, hasInlineImages: boolean }} + */ +export function stripInlineBase64Images(content) { + if (!content || typeof content !== 'string') { + return { sanitizedContent: content || '', hasInlineImages: false }; + } + + const markdownInlineBase64ImageRegex = + /!\[[^\]]*]\(\s*data:image\/[a-zA-Z0-9.+-]+;base64,[^)]+\s*\)/gi; + const sanitizedContent = content.replace(markdownInlineBase64ImageRegex, ''); + const hasInlineImages = sanitizedContent !== content; + + return { sanitizedContent, hasInlineImages }; +} + /** * Strip unsupported markdown formatting based on channel capabilities. * Uses MARKDOWN_PATTERNS from editor constants. diff --git a/app/javascript/dashboard/helper/specs/editorHelper.spec.js b/app/javascript/dashboard/helper/specs/editorHelper.spec.js index ca61c1bab..f558dd213 100644 --- a/app/javascript/dashboard/helper/specs/editorHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/editorHelper.spec.js @@ -15,6 +15,7 @@ import { getMenuAnchor, calculateMenuPosition, stripUnsupportedFormatting, + stripInlineBase64Images, } from '../editorHelper'; import { FORMATTING } from 'dashboard/constants/editor'; import { EditorState } from '@chatwoot/prosemirror-schema'; @@ -423,6 +424,36 @@ describe('extractTextFromMarkdown', () => { }); }); +describe('stripInlineBase64Images', () => { + it('removes markdown data:image base64 images and sets hasInlineImages', () => { + const content = + 'Hello\n![x](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAE)\nWorld'; + const { sanitizedContent, hasInlineImages } = + stripInlineBase64Images(content); + + expect(hasInlineImages).toBe(true); + expect(sanitizedContent).not.toContain('data:image/png;base64'); + expect(sanitizedContent).toContain('Hello'); + expect(sanitizedContent).toContain('World'); + }); + + it('leaves hosted image markdown unchanged', () => { + const content = '![](https://example.com/logo.png)'; + const { sanitizedContent, hasInlineImages } = + stripInlineBase64Images(content); + + expect(hasInlineImages).toBe(false); + expect(sanitizedContent).toBe(content); + }); + + it('returns empty hasInlineImages for empty input', () => { + expect(stripInlineBase64Images('')).toEqual({ + sanitizedContent: '', + hasInlineImages: false, + }); + }); +}); + describe('insertAtCursor', () => { it('should return undefined if editorView is not provided', () => { const result = insertAtCursor(undefined, schema.text('Hello'), 0); diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index d885cf8ce..bfbd920a7 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -68,7 +68,8 @@ "API_SUCCESS": "Signature saved successfully", "IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again", "IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature", - "IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB" + "IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB", + "INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature." }, "MESSAGE_SIGNATURE": { "LABEL": "Message Signature", diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue index fbaed06c1..b0dab9774 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue @@ -1,5 +1,8 @@