From df4c8cf58b7b593e0eb0e0206dd8ef048ddaff77 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Thu, 11 Dec 2025 19:58:59 +0530
Subject: [PATCH] chore: Strip unsupported signature formatting by channel
(#13046)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Pull Request Template
## Description
1. This PR is an enhancement to
https://github.com/chatwoot/chatwoot/pull/13045
It strips unsupported formatting from **message signatures** based on
each channel’s formatting capabilities defined in the `FORMATTING`
config
2. Remove usage of plain editor in Compose new conversation modal
Only the following signature elements are considered:
bold (strong), italic (em), links (link), images (image).
Any formatting not supported by the target channel is automatically
removed before the signature is appended.
Channel-wise Signature Formatting Support
Channel | Keeps in Signature | Strips from Signature
-- | -- | --
Email | bold, italic, links, images | —
WebWidget | bold, italic, links, images | —
API | bold, italic | links, images
WhatsApp | bold, italic | links, images
Telegram | bold, italic, links | images
Facebook | bold, italic | links, images
Instagram | bold, italic | links, images
Line | bold, italic | links, images
SMS | — | everything
Twilio SMS | — | everything
Twitter/X | — | everything
📝 Note
Message signatures only support bold, italic, links, and
images.
Other formatting options available in the editor (lists, code blocks,
strike-through, etc.) do not apply to signatures and
are ignored.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/d325ab86ca514c6d8f90dfe72a8928dd
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth
---
.../components-next/Editor/Editor.vue | 2 +
.../components/ComposeNewConversationForm.vue | 25 +++-
.../components/MessageEditor.vue | 119 ++++--------------
.../components/widgets/WootWriter/Editor.vue | 31 ++++-
.../widgets/conversation/ReplyBox.vue | 22 +++-
app/javascript/dashboard/constants/editor.js | 12 +-
.../dashboard/helper/editorHelper.js | 111 ++++++++++++----
app/javascript/dashboard/helper/inbox.js | 5 +
.../helper/specs/editorHelper.spec.js | 110 ++++++++++++----
9 files changed, 270 insertions(+), 167 deletions(-)
diff --git a/app/javascript/dashboard/components-next/Editor/Editor.vue b/app/javascript/dashboard/components-next/Editor/Editor.vue
index ff1c616de..90b7a0c31 100644
--- a/app/javascript/dashboard/components-next/Editor/Editor.vue
+++ b/app/javascript/dashboard/components-next/Editor/Editor.vue
@@ -24,6 +24,7 @@ const props = defineProps({
allowSignature: { type: Boolean, default: false },
sendWithSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
+ medium: { type: String, default: '' },
});
const emit = defineEmits(['update:modelValue']);
@@ -106,6 +107,7 @@ watch(
:allow-signature="allowSignature"
:send-with-signature="sendWithSignature"
:channel-type="channelType"
+ :medium="medium"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue
index 52ca95a07..77267707b 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue
@@ -6,6 +6,7 @@ import { INBOX_TYPES } from 'dashboard/helper/inbox';
import {
appendSignature,
removeSignature,
+ getEffectiveChannelType,
} from 'dashboard/helper/editorHelper';
import {
buildContactableInboxesList,
@@ -86,6 +87,12 @@ const whatsappMessageTemplates = computed(() =>
const inboxChannelType = computed(() => props.targetInbox?.channelType || '');
+const inboxMedium = computed(() => props.targetInbox?.medium || '');
+
+const effectiveChannelType = computed(() =>
+ getEffectiveChannelType(inboxChannelType.value, inboxMedium.value)
+);
+
const validationRules = computed(() => ({
selectedContact: { required },
targetInbox: { required },
@@ -202,7 +209,11 @@ const removeSignatureFromMessage = () => {
// Always remove the signature from message content when inbox/contact is removed
// to ensure no leftover signature content remains
if (props.messageSignature) {
- state.message = removeSignature(state.message, props.messageSignature);
+ state.message = removeSignature(
+ state.message,
+ props.messageSignature,
+ effectiveChannelType.value
+ );
}
};
@@ -214,9 +225,9 @@ const removeTargetInbox = value => {
};
const clearSelectedContact = () => {
+ removeSignatureFromMessage();
emit('clearSelectedContact');
state.attachedFiles = [];
- removeSignatureFromMessage();
};
const onClickInsertEmoji = emoji => {
@@ -227,12 +238,16 @@ const handleAddSignature = signature => {
state.message = appendSignature(
state.message,
signature,
- inboxChannelType.value
+ effectiveChannelType.value
);
};
const handleRemoveSignature = signature => {
- state.message = removeSignature(state.message, signature);
+ state.message = removeSignature(
+ state.message,
+ signature,
+ effectiveChannelType.value
+ );
};
const handleAttachFile = files => {
@@ -356,10 +371,10 @@ const shouldShowMessageEditor = computed(() => {
v-model="state.message"
:message-signature="messageSignature"
:send-with-signature="sendWithSignature"
- :is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget"
:has-errors="validationStates.isMessageInvalid"
:has-attachments="state.attachedFiles.length > 0"
:channel-type="inboxChannelType"
+ :medium="targetInbox?.medium || ''"
/>
-import { ref, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
-import {
- appendSignature,
- removeSignature,
-} from 'dashboard/helper/editorHelper';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
-import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
-import CannedResponse from 'dashboard/components/widgets/conversation/CannedResponse.vue';
-const props = defineProps({
- isEmailOrWebWidgetInbox: { type: Boolean, required: true },
+defineProps({
hasErrors: { type: Boolean, default: false },
hasAttachments: { type: Boolean, default: false },
sendWithSignature: { type: Boolean, default: false },
messageSignature: { type: String, default: '' },
channelType: { type: String, default: '' },
+ medium: { type: String, default: '' },
});
const { t } = useI18n();
@@ -25,98 +18,28 @@ const modelValue = defineModel({
type: String,
default: '',
});
-
-const state = ref({
- hasSlashCommand: false,
- showMentions: false,
- mentionSearchKey: '',
-});
-
-watch(
- modelValue,
- newValue => {
- if (props.isEmailOrWebWidgetInbox) return;
-
- const bodyWithoutSignature = newValue
- ? removeSignature(newValue, props.messageSignature)
- : '';
-
- // Check if message starts with slash
- const startsWithSlash = bodyWithoutSignature.startsWith('/');
-
- // Update slash command and mentions state
- state.value = {
- ...state.value,
- hasSlashCommand: startsWithSlash,
- showMentions: startsWithSlash,
- mentionSearchKey: startsWithSlash ? bodyWithoutSignature.slice(1) : '',
- };
- },
- { immediate: true }
-);
-
-const hideMention = () => {
- state.value.showMentions = false;
-};
-
-const replaceText = async message => {
- // Only append signature on replace if sendWithSignature is true
- const finalMessage = props.sendWithSignature
- ? appendSignature(message, props.messageSignature, props.channelType)
- : message;
-
- await nextTick();
- modelValue.value = finalMessage;
-};
-
-
-
-
-
-
+
diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
index 318727acc..7a2a489ac 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
@@ -54,6 +54,7 @@ import {
getFormattingForEditor,
getSelectionCoords,
calculateMenuPosition,
+ getEffectiveChannelType,
} from 'dashboard/helper/editorHelper';
import {
hasPressedEnterAndNotCmdOrShift,
@@ -81,6 +82,7 @@ const props = defineProps({
// are triggered except when this flag is true
allowSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
+ medium: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
focusOnMount: { type: Boolean, default: true },
});
@@ -105,10 +107,16 @@ const TYPING_INDICATOR_IDLE_TIME = 4000;
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const DEFAULT_FORMATTING = 'Context::Default';
+const effectiveChannelType = computed(() =>
+ getEffectiveChannelType(props.channelType, props.medium)
+);
+
const editorSchema = computed(() => {
if (!props.channelType) return messageSchema;
- const formatType = props.isPrivate ? DEFAULT_FORMATTING : props.channelType;
+ const formatType = props.isPrivate
+ ? DEFAULT_FORMATTING
+ : effectiveChannelType.value;
const formatting = getFormattingForEditor(formatType);
return buildMessageSchema(formatting.marks, formatting.nodes);
});
@@ -116,7 +124,7 @@ const editorSchema = computed(() => {
const editorMenuOptions = computed(() => {
const formatType = props.isPrivate
? DEFAULT_FORMATTING
- : props.channelType || DEFAULT_FORMATTING;
+ : effectiveChannelType.value || DEFAULT_FORMATTING;
const formatting = getFormattingForEditor(formatType);
return formatting.menu;
});
@@ -301,8 +309,13 @@ function isBodyEmpty(content) {
// if the signature is present, we need to remove it before checking
// note that we don't update the editorView, so this is safe
+ // Use effective channel type to match how signature was appended
const bodyWithoutSignature = props.signature
- ? removeSignatureHelper(content, props.signature)
+ ? removeSignatureHelper(
+ content,
+ props.signature,
+ effectiveChannelType.value
+ )
: content;
// trimming should remove all the whitespaces, so we can check the length
@@ -370,7 +383,11 @@ function addSignature() {
// 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
const contentWasEmpty = isBodyEmpty(content);
- content = appendSignature(content, props.signature, props.channelType);
+ content = appendSignature(
+ content,
+ props.signature,
+ effectiveChannelType.value
+ );
// need to reload first, ensuring that the editorView is updated
reloadState(content);
@@ -382,7 +399,11 @@ function addSignature() {
function removeSignature() {
if (!props.signature) return;
let content = props.modelValue;
- content = removeSignatureHelper(content, props.signature);
+ content = removeSignatureHelper(
+ content,
+ props.signature,
+ effectiveChannelType.value
+ );
// reload the state, ensuring that the editorView is updated
reloadState(content);
}
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index 0e698e0c6..6f955ffd5 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -43,6 +43,7 @@ import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
import {
appendSignature,
removeSignature,
+ getEffectiveChannelType,
} from 'dashboard/helper/editorHelper';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@@ -564,9 +565,13 @@ export default {
return message;
}
+ const effectiveChannelType = getEffectiveChannelType(
+ this.channelType,
+ this.inbox?.medium || ''
+ );
return this.sendWithSignature
- ? appendSignature(message, this.messageSignature, this.channelType)
- : removeSignature(message, this.messageSignature);
+ ? appendSignature(message, this.messageSignature, effectiveChannelType)
+ : removeSignature(message, this.messageSignature, effectiveChannelType);
},
removeFromDraft() {
if (this.conversationIdByRoute) {
@@ -757,10 +762,14 @@ export default {
// 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,
- this.channelType
+ effectiveChannelType
);
}
@@ -800,10 +809,14 @@ export default {
this.message = '';
if (this.sendWithSignature && !this.isPrivate) {
// if signature is enabled, append it to the message
+ const effectiveChannelType = getEffectiveChannelType(
+ this.channelType,
+ this.inbox?.medium || ''
+ );
this.message = appendSignature(
this.message,
this.messageSignature,
- this.channelType
+ effectiveChannelType
);
}
this.attachedFiles = [];
@@ -1121,6 +1134,7 @@ export default {
:signature="messageSignature"
allow-signature
:channel-type="channelType"
+ :medium="inbox.medium"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
diff --git a/app/javascript/dashboard/constants/editor.js b/app/javascript/dashboard/constants/editor.js
index eaec36371..087caa99f 100644
--- a/app/javascript/dashboard/constants/editor.js
+++ b/app/javascript/dashboard/constants/editor.js
@@ -210,7 +210,12 @@ export const MARKDOWN_PATTERNS = [
type: 'em', // PM: em, eg: *italic* or _italic_
patterns: [
{ pattern: /(? arr.includes(key);
+
+ // Define stripping rules: [condition, pattern, replacement]
+ const rules = [
+ [!has(nodes, 'image'), /!\[.*?\]\(.*?\)/g, ''],
+ [!has(marks, 'link'), /\[([^\]]+)\]\([^)]+\)/g, '$1'],
+ [!has(nodes, 'codeBlock'), /```[\s\S]*?```/g, ''],
+ [!has(marks, 'code'), /`([^`]+)`/g, '$1'],
+ [!has(marks, 'strong'), /\*\*([^*]+)\*\*/g, '$1'],
+ [!has(marks, 'strong'), /__([^_]+)__/g, '$1'],
+ [!has(marks, 'em'), /\*([^*]+)\*/g, '$1'],
+ // Match _text_ only at word boundaries (whitespace/string start/end)
+ // Preserves underscores in URLs (e.g., https://example.com/path_name) and variable names
+ [
+ !has(marks, 'em'),
+ /(?<=^|[\s])_([^_\s][^_]*[^_\s]|[^_\s])_(?=$|[\s])/g,
+ '$1',
+ ],
+ [!has(marks, 'strike'), /~~([^~]+)~~/g, '$1'],
+ [!has(nodes, 'blockquote'), /^>\s?/gm, ''],
+ [!has(nodes, 'bulletList'), /^[-*+]\s+/gm, ''],
+ [!has(nodes, 'orderedList'), /^\d+\.\s+/gm, ''],
+ ];
+
+ const result = rules.reduce(
+ (text, [shouldStrip, pattern, replacement]) =>
+ shouldStrip ? text.replace(pattern, replacement) : text,
+ markdown
+ );
+
+ return result
+ .split('\n')
+ .map(line => line.trim())
+ .filter(Boolean)
+ .join('\n')
+ .replace(/\n{2,}/g, '\n')
+ .trim();
+}
+
/**
* The delimiter used to separate the signature from the rest of the body.
* @type {string}
@@ -97,29 +144,36 @@ export function findSignatureInBody(body, signature) {
}
/**
- * Checks if the channel supports image signatures.
+ * Gets the effective channel type for formatting purposes.
+ * For Twilio channels, returns WhatsApp or Twilio based on medium.
*
- * @param {string} channelType - The channel type.
- * @returns {boolean} - True if the channel supports image signatures.
+ * @param {string} channelType - The channel type
+ * @param {string} medium - Optional. The medium for Twilio channels (sms/whatsapp)
+ * @returns {string} - The effective channel type for formatting
*/
-export function supportsImageSignature(channelType) {
- return CHANNEL_WITH_RICH_SIGNATURE.includes(channelType);
+export function getEffectiveChannelType(channelType, medium) {
+ if (channelType === INBOX_TYPES.TWILIO) {
+ return medium === TWILIO_CHANNEL_MEDIUM.WHATSAPP
+ ? INBOX_TYPES.WHATSAPP
+ : INBOX_TYPES.TWILIO;
+ }
+ return channelType;
}
/**
* Appends the signature to the body, separated by the signature delimiter.
- * Automatically strips images for channels that don't support image signatures.
+ * Automatically strips unsupported formatting based on channel capabilities.
*
* @param {string} body - The body to append the signature to.
* @param {string} signature - The signature to append.
- * @param {string} channelType - Optional. The channel type to determine if images should be stripped.
+ * @param {string} channelType - Optional. The effective channel type to determine supported formatting.
+ * For Twilio channels, pass the result of getEffectiveChannelType().
* @returns {string} - The body with the signature appended.
*/
export function appendSignature(body, signature, channelType) {
- // For channels that don't support images, strip markdown formatting
- const shouldStripImages = channelType && !supportsImageSignature(channelType);
- const preparedSignature = shouldStripImages
- ? extractTextFromMarkdown(signature)
+ // Strip only unsupported formatting based on channel capabilities
+ const preparedSignature = channelType
+ ? stripUnsupportedSignatureMarkdown(signature, channelType)
: signature;
const cleanedSignature = cleanSignature(preparedSignature);
// if signature is already present, return body
@@ -132,21 +186,28 @@ export function appendSignature(body, signature, channelType) {
/**
* Removes the signature from the body, along with the signature delimiter.
- * Tries to find both the original signature and the stripped version (for non-image channels).
+ * Tries to find both the original signature and the stripped version.
*
* @param {string} body - The body to remove the signature from.
* @param {string} signature - The signature to remove.
+ * @param {string} channelType - Optional. The effective channel type for channel-specific stripping.
+ * For Twilio channels, pass the result of getEffectiveChannelType().
* @returns {string} - The body with the signature removed.
*/
-export function removeSignature(body, signature) {
- // Build list of signatures to try: original first, then stripped version
- // Always try both to handle cases where channelType is unknown or inbox is being removed
+export function removeSignature(body, signature, channelType) {
+ // Build list of signatures to try: original, channel-stripped, and fully stripped
const cleanedSignature = cleanSignature(signature);
- const strippedSignature = cleanSignature(extractTextFromMarkdown(signature));
- const signaturesToTry =
- cleanedSignature === strippedSignature
- ? [cleanedSignature]
- : [cleanedSignature, strippedSignature];
+ const channelStripped = channelType
+ ? cleanSignature(stripUnsupportedSignatureMarkdown(signature, channelType))
+ : null;
+ const fullyStripped = cleanSignature(extractTextFromMarkdown(signature));
+
+ // Try signatures in order: original → channel-specific → fully stripped
+ const signaturesToTry = [
+ cleanedSignature,
+ channelStripped,
+ fullyStripped,
+ ].filter((sig, i, arr) => sig && arr.indexOf(sig) === i); // Remove nulls and duplicates
// Find the first matching signature
const signatureIndex = signaturesToTry.reduce(
diff --git a/app/javascript/dashboard/helper/inbox.js b/app/javascript/dashboard/helper/inbox.js
index 501375ab7..05823e8a7 100644
--- a/app/javascript/dashboard/helper/inbox.js
+++ b/app/javascript/dashboard/helper/inbox.js
@@ -13,6 +13,11 @@ export const INBOX_TYPES = {
VOICE: 'Channel::Voice',
};
+export const TWILIO_CHANNEL_MEDIUM = {
+ WHATSAPP: 'whatsapp',
+ SMS: 'sms',
+};
+
const INBOX_ICON_MAP_FILL = {
[INBOX_TYPES.WEB]: 'i-ri-global-fill',
[INBOX_TYPES.FB]: 'i-ri-messenger-fill',
diff --git a/app/javascript/dashboard/helper/specs/editorHelper.spec.js b/app/javascript/dashboard/helper/specs/editorHelper.spec.js
index 07dfbf1e5..1ae765ca7 100644
--- a/app/javascript/dashboard/helper/specs/editorHelper.spec.js
+++ b/app/javascript/dashboard/helper/specs/editorHelper.spec.js
@@ -5,7 +5,7 @@ import {
replaceSignature,
cleanSignature,
extractTextFromMarkdown,
- supportsImageSignature,
+ stripUnsupportedSignatureMarkdown,
insertAtCursor,
findNodeToInsertImage,
setURLWithQueryAndSize,
@@ -145,10 +145,63 @@ describe('appendSignature', () => {
});
});
+describe('stripUnsupportedSignatureMarkdown', () => {
+ const richSignature =
+ '**Bold** _italic_ [link](http://example.com) ';
+
+ it('keeps all formatting for Email channel (supports image, link, strong, em)', () => {
+ const result = stripUnsupportedSignatureMarkdown(
+ richSignature,
+ 'Channel::Email'
+ );
+ expect(result).toContain('**Bold**');
+ expect(result).toContain('_italic_');
+ expect(result).toContain('[link](http://example.com)');
+ expect(result).toContain('');
+ });
+ it('strips images but keeps bold/italic for Api channel', () => {
+ const result = stripUnsupportedSignatureMarkdown(
+ richSignature,
+ 'Channel::Api'
+ );
+ expect(result).toContain('**Bold**');
+ expect(result).toContain('_italic_');
+ expect(result).toContain('link'); // link text kept
+ expect(result).not.toContain('[link]('); // link syntax removed
+ expect(result).not.toContain('; // image removed
+ });
+ it('strips images but keeps bold/italic/link for Telegram channel', () => {
+ const result = stripUnsupportedSignatureMarkdown(
+ richSignature,
+ 'Channel::Telegram'
+ );
+ expect(result).toContain('**Bold**');
+ expect(result).toContain('_italic_');
+ expect(result).toContain('[link](http://example.com)');
+ expect(result).not.toContain(';
+ });
+ it('strips all formatting for SMS channel', () => {
+ const result = stripUnsupportedSignatureMarkdown(
+ richSignature,
+ 'Channel::Sms'
+ );
+ expect(result).toContain('Bold');
+ expect(result).toContain('italic');
+ expect(result).toContain('link');
+ expect(result).not.toContain('**');
+ expect(result).not.toContain('_');
+ expect(result).not.toContain('[');
+ expect(result).not.toContain(';
+ });
+ it('returns empty string for empty input', () => {
+ expect(stripUnsupportedSignatureMarkdown('', 'Channel::Api')).toBe('');
+ expect(stripUnsupportedSignatureMarkdown(null, 'Channel::Api')).toBe('');
+ });
+});
+
describe('appendSignature with channelType', () => {
const signatureWithImage =
'Thanks\n';
- const strippedSignature = 'Thanks';
it('keeps images for Email channel', () => {
const result = appendSignature(
@@ -166,24 +219,31 @@ describe('appendSignature with channelType', () => {
);
expect(result).toContain(';
});
- it('strips images for Api channel', () => {
+ it('strips images but keeps text for Api channel', () => {
const result = appendSignature('Hello', signatureWithImage, 'Channel::Api');
expect(result).not.toContain(';
- expect(result).toContain(strippedSignature);
+ expect(result).toContain('Thanks');
});
- it('strips images for WhatsApp channel', () => {
+ it('strips images but keeps text for WhatsApp channel', () => {
const result = appendSignature(
'Hello',
signatureWithImage,
'Channel::Whatsapp'
);
expect(result).not.toContain(';
- expect(result).toContain(strippedSignature);
+ expect(result).toContain('Thanks');
});
it('keeps images when channelType is not provided', () => {
const result = appendSignature('Hello', signatureWithImage);
expect(result).toContain(';
});
+ it('keeps bold/italic for channels that support them', () => {
+ const boldSignature = '**Bold** *italic* Thanks';
+ const result = appendSignature('Hello', boldSignature, 'Channel::Api');
+ // Api supports strong and em
+ expect(result).toContain('**Bold**');
+ expect(result).toContain('*italic*');
+ });
});
describe('cleanSignature', () => {
@@ -331,24 +391,6 @@ describe('extractTextFromMarkdown', () => {
});
});
-describe('supportsImageSignature', () => {
- it('returns true for Email channel', () => {
- expect(supportsImageSignature('Channel::Email')).toBe(true);
- });
- it('returns true for WebWidget channel', () => {
- expect(supportsImageSignature('Channel::WebWidget')).toBe(true);
- });
- it('returns false for Api channel', () => {
- expect(supportsImageSignature('Channel::Api')).toBe(false);
- });
- it('returns false for WhatsApp channel', () => {
- expect(supportsImageSignature('Channel::Whatsapp')).toBe(false);
- });
- it('returns false for Telegram channel', () => {
- expect(supportsImageSignature('Channel::Telegram')).toBe(false);
- });
-});
-
describe('insertAtCursor', () => {
it('should return undefined if editorView is not provided', () => {
const result = insertAtCursor(undefined, schema.text('Hello'), 0);
@@ -884,6 +926,26 @@ describe('stripUnsupportedFormatting', () => {
);
});
+ it('preserves underscores in URLs and mid-word positions', () => {
+ // Underscores in URLs should not be stripped as italic formatting
+ expect(
+ stripUnsupportedFormatting(
+ 'https://www.chatwoot.com/new_first_second-third/ssd',
+ emptySchema
+ )
+ ).toBe('https://www.chatwoot.com/new_first_second-third/ssd');
+
+ // Underscores in variable names should not be stripped
+ expect(
+ stripUnsupportedFormatting('some_variable_name', emptySchema)
+ ).toBe('some_variable_name');
+
+ // But actual italic formatting with spaces should still be stripped
+ expect(
+ stripUnsupportedFormatting('hello _world_ there', emptySchema)
+ ).toBe('hello world there');
+ });
+
it('strips inline code formatting', () => {
expect(stripUnsupportedFormatting('`inline code`', emptySchema)).toBe(
'inline code'