diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
index a1a521c75..1d32bc4d3 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
@@ -24,6 +24,7 @@ import { useTrack } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
import { useMapGetter } from 'dashboard/composables/store';
+import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { CONVERSATION_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
@@ -47,7 +48,6 @@ import {
import {
findNodeToInsertImage,
getContentNode,
- cleanSignature,
insertAtCursor,
scrollCursorIntoView,
setURLWithQueryAndSize,
@@ -146,6 +146,8 @@ const createState = (content, placeholder, plugins = [], methods = {}) => {
const { isEditorHotKeyEnabled, fetchSignatureFlagFromUISettings } =
useUISettings();
+const { formatMessage } = useMessageFormatter();
+
const currentUser = useMapGetter('getCurrentUser');
const typingIndicator = createTypingIndicator(
@@ -273,8 +275,7 @@ 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
+ // this is considered the source of truth for signature display
if (props.allowSignature && !props.isPrivate && props.channelType) {
return fetchSignatureFlagFromUISettings(props.channelType);
}
@@ -282,6 +283,23 @@ const sendWithSignature = computed(() => {
return false;
});
+const signaturePosition = computed(() => {
+ return currentUser.value?.ui_settings?.signature_position || 'top';
+});
+
+const signatureSeparator = computed(() => {
+ return currentUser.value?.ui_settings?.signature_separator || 'blank';
+});
+
+const shouldShowSignaturePreview = computed(() => {
+ return sendWithSignature.value && props.signature;
+});
+
+const formattedSignature = computed(() => {
+ if (!props.signature) return '';
+ return formatMessage(props.signature, false, false);
+});
+
watch(showUserMentions, updatedValue => {
emit('toggleUserMention', props.isPrivate && updatedValue);
});
@@ -298,27 +316,10 @@ watch(showToolsMenu, updatedValue => {
function focusEditorInputField(pos = 'end') {
const { tr } = editorView.state;
- // Check if signature is at start and adjust cursor position accordingly
- const signaturePosition =
- currentUser.value?.ui_settings?.signature_position || 'top';
- const hasSignature = sendWithSignature.value && props.signature;
-
- let selection;
- if (pos === 'end' || !hasSignature || signaturePosition !== 'top') {
- selection =
- pos === 'end' ? Selection.atEnd(tr.doc) : Selection.atStart(tr.doc);
- } else {
- // Position cursor after signature when signature is at start
- const signatureLength = props.signature
- ? cleanSignature(props.signature).length
- : 0;
- const separatorLength =
- currentUser.value?.ui_settings?.signature_separator === '--' ? 6 : 2; // "\n--\n" vs "\n\n"
- const cursorPos = signatureLength + separatorLength;
- selection = Selection.near(
- tr.doc.resolve(Math.min(cursorPos, tr.doc.content.size))
- );
- }
+ // Signature is now displayed as read-only preview outside the editor,
+ // so cursor positioning is straightforward
+ const selection =
+ pos === 'end' ? Selection.atEnd(tr.doc) : Selection.atStart(tr.doc);
editorView.dispatch(tr.setSelection(selection));
editorView.focus();
@@ -765,7 +766,33 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
hidden
@change="onFileChange"
/>
+
+
@import '@chatwoot/prosemirror-schema/src/styles/base.scss';
+.signature-preview {
+ @apply px-1 py-1 text-n-slate-10 text-sm pointer-events-none select-none opacity-70;
+
+ &--top {
+ @apply border-b border-n-weak pb-1;
+
+ .signature-separator {
+ @apply text-n-slate-9 mt-1 mb-0;
+ }
+ }
+
+ &--bottom {
+ @apply border-t border-n-weak pt-1 mt-2;
+
+ .signature-separator {
+ @apply text-n-slate-9 mb-1 mt-0;
+ }
+ }
+
+ .signature-label {
+ @apply text-xs text-n-slate-9 mb-1;
+ }
+
+ .signature-content {
+ @apply break-words;
+
+ :deep(p) {
+ @apply m-0 text-n-slate-10;
+ }
+
+ :deep(a) {
+ @apply text-n-slate-10 no-underline;
+ }
+ }
+}
+
.ProseMirror-menubar-wrapper {
@apply flex flex-col gap-3;
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index 766cb4c0b..c5f23f090 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -4,6 +4,7 @@ import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useTrack } from 'dashboard/composables';
+import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -42,12 +43,7 @@ import {
} from 'dashboard/helper/quotedEmailHelper';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
-import {
- appendSignature,
- removeSignature,
- getEffectiveChannelType,
- extractTextFromMarkdown,
-} from 'dashboard/helper/editorHelper';
+import { appendSignature } from 'dashboard/helper/editorHelper';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
@@ -92,6 +88,8 @@ export default {
fetchQuotedReplyFlagFromUISettings,
} = useUISettings();
+ const { formatMessage } = useMessageFormatter();
+
const replyEditor = useTemplateRef('replyEditor');
return {
@@ -101,6 +99,7 @@ export default {
setQuotedReplyFlagForInbox,
fetchQuotedReplyFlagFromUISettings,
replyEditor,
+ formatMessage,
};
},
data() {
@@ -418,11 +417,24 @@ export default {
return false;
},
- // ensure that the signature is plain text depending on `showRichContentEditor`
- signatureToApply() {
- return this.showRichContentEditor
- ? this.messageSignature
- : extractTextFromMarkdown(this.messageSignature);
+ // Signature preview for non-rich editor (WhatsApp, etc.)
+ shouldShowSignaturePreview() {
+ return (
+ this.sendWithSignature &&
+ this.messageSignature &&
+ !this.isPrivate &&
+ !this.showRichContentEditor
+ );
+ },
+ signaturePosition() {
+ return this.currentUser?.ui_settings?.signature_position || 'top';
+ },
+ signatureSeparator() {
+ return this.currentUser?.ui_settings?.signature_separator || 'blank';
+ },
+ formattedSignature() {
+ if (!this.messageSignature) return '';
+ return this.formatMessage(this.messageSignature, false, false);
},
},
watch: {
@@ -606,31 +618,6 @@ export default {
this.message = messageFromStore;
}
},
- toggleSignatureForDraft(message) {
- if (this.isPrivate) {
- return message;
- }
- if (this.showRichContentEditor) {
- const effectiveChannelType = getEffectiveChannelType(
- this.channelType,
- this.inbox?.medium || ''
- );
- return this.sendWithSignature
- ? appendSignature(
- message,
- this.messageSignature,
- effectiveChannelType
- )
- : removeSignature(
- message,
- this.messageSignature,
- effectiveChannelType
- );
- }
- return this.sendWithSignature
- ? appendSignature(message, this.signatureToApply)
- : removeSignature(message, this.signatureToApply);
- },
removeFromDraft() {
if (this.conversationIdByRoute) {
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
@@ -686,6 +673,18 @@ export default {
this.isEditorHotKeyEnabled(selectedKey)
);
},
+ applySignatureToMessage(message) {
+ if (!this.sendWithSignature || !this.messageSignature) {
+ return message;
+ }
+ const { signature_position, signature_separator } =
+ this.currentUser?.ui_settings || {};
+ const signatureSettings = {
+ position: signature_position || 'top',
+ separator: signature_separator || 'blank',
+ };
+ return appendSignature(message, this.messageSignature, signatureSettings);
+ },
onPaste(e) {
// Don't handle paste if compose new conversation modal is open
if (this.newConversationModalActive) {
@@ -999,8 +998,10 @@ export default {
getMultipleMessagesPayload(message) {
const multipleMessagePayload = [];
- if (this.attachedFiles && this.attachedFiles.length) {
- let caption = this.isAnInstagramChannel ? '' : message;
+ const messageWithSignature = this.applySignatureToMessage(message);
+
+ if (this.attachedFiles?.length) {
+ let caption = this.isAnInstagramChannel ? '' : messageWithSignature;
this.attachedFiles.forEach(attachment => {
const attachedFile = this.globalConfig.directUploadsEnabled
? attachment.blobSignedId
@@ -1020,8 +1021,7 @@ export default {
});
}
- const hasNoAttachments =
- !this.attachedFiles || !this.attachedFiles.length;
+ const hasNoAttachments = !this.attachedFiles?.length;
// For Instagram, we need a separate text message
// For WhatsApp, we only need a text message if there are no attachments
if (
@@ -1030,7 +1030,7 @@ export default {
) {
let messagePayload = {
conversationId: this.currentChat.id,
- message,
+ message: messageWithSignature,
private: false,
sender: this.sender,
};
@@ -1044,18 +1044,8 @@ export default {
},
getMessagePayload(message) {
let finalMessage = this.getMessageWithQuotedEmailText(message);
- if (this.sendWithSignature && !this.isPrivate && this.messageSignature) {
- const { signature_position, signature_separator } =
- this.currentUser?.ui_settings || {};
- const signatureSettings = {
- position: signature_position || 'top',
- separator: signature_separator || 'blank',
- };
- finalMessage = appendSignature(
- message,
- this.messageSignature,
- signatureSettings
- );
+ if (!this.isPrivate) {
+ finalMessage = this.applySignatureToMessage(finalMessage);
}
let messagePayload = {
@@ -1066,7 +1056,7 @@ export default {
};
messagePayload = this.setReplyToInPayload(messagePayload);
- if (this.attachedFiles && this.attachedFiles.length) {
+ if (this.attachedFiles?.length) {
messagePayload.files = [];
messagePayload.isRecordedAudio = [];
this.attachedFiles.forEach(attachment => {
@@ -1222,18 +1212,51 @@ export default {
@play="recordingAudioState = 'playing'"
@pause="recordingAudioState = 'paused'"
/>
-
+
+
+
+
+ {{ $t('CONVERSATION.FOOTER.SIGNATURE_LABEL_TOP') }}
+
+
+
+ {{ signatureSeparator }}
+
+
+
+
+
+
+ {{ $t('CONVERSATION.FOOTER.SIGNATURE_LABEL_BOTTOM') }}
+
+
+ {{ signatureSeparator }}
+
+
+
+