mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 02:32:29 +00:00
This PR adds the ability to include the thread history as a quoted text ## Preview https://github.com/user-attachments/assets/c96a85e5-8ac8-4021-86ca-57509b4eea9f
1388 lines
43 KiB
Vue
1388 lines
43 KiB
Vue
<script>
|
|
import { defineAsyncComponent, useTemplateRef } from 'vue';
|
|
import { mapGetters } from 'vuex';
|
|
import { useAlert } from 'dashboard/composables';
|
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
import { useTrack } from 'dashboard/composables';
|
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
|
|
import CannedResponse from './CannedResponse.vue';
|
|
import ReplyToMessage from './ReplyToMessage.vue';
|
|
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
|
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
|
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
|
import ReplyEmailHead from './ReplyEmailHead.vue';
|
|
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
|
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
|
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
|
import ReplyBoxBanner from './ReplyBoxBanner.vue';
|
|
import QuotedEmailPreview from './QuotedEmailPreview.vue';
|
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
|
import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
|
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
|
import {
|
|
getMessageVariables,
|
|
getUndefinedVariablesInMessage,
|
|
replaceVariablesInMessage,
|
|
} from '@chatwoot/utils';
|
|
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
|
import ContentTemplates from './ContentTemplates/ContentTemplatesModal.vue';
|
|
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
|
import { trimContent, debounce, getRecipients } from '@chatwoot/utils';
|
|
import wootConstants from 'dashboard/constants/globals';
|
|
import {
|
|
extractQuotedEmailText,
|
|
buildQuotedEmailHeader,
|
|
truncatePreviewText,
|
|
appendQuotedTextToMessage,
|
|
} from 'dashboard/helper/quotedEmailHelper';
|
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
|
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
|
import {
|
|
appendSignature,
|
|
removeSignature,
|
|
replaceSignature,
|
|
extractTextFromMarkdown,
|
|
} from 'dashboard/helper/editorHelper';
|
|
|
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
|
import { emitter } from 'shared/helpers/mitt';
|
|
const EmojiInput = defineAsyncComponent(
|
|
() => import('shared/components/emoji/EmojiInput.vue')
|
|
);
|
|
|
|
export default {
|
|
components: {
|
|
ArticleSearchPopover,
|
|
AttachmentPreview,
|
|
AudioRecorder,
|
|
CannedResponse,
|
|
ReplyBoxBanner,
|
|
EmojiInput,
|
|
MessageSignatureMissingAlert,
|
|
ReplyBottomPanel,
|
|
ReplyEmailHead,
|
|
ReplyToMessage,
|
|
ReplyTopPanel,
|
|
ResizableTextArea,
|
|
ContentTemplates,
|
|
WhatsappTemplates,
|
|
WootMessageEditor,
|
|
QuotedEmailPreview,
|
|
},
|
|
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
|
props: {
|
|
popOutReplyBox: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
emits: ['update:popOutReplyBox'],
|
|
setup() {
|
|
const {
|
|
uiSettings,
|
|
updateUISettings,
|
|
isEditorHotKeyEnabled,
|
|
fetchSignatureFlagFromUISettings,
|
|
setQuotedReplyFlagForInbox,
|
|
fetchQuotedReplyFlagFromUISettings,
|
|
} = useUISettings();
|
|
|
|
const replyEditor = useTemplateRef('replyEditor');
|
|
|
|
return {
|
|
uiSettings,
|
|
updateUISettings,
|
|
isEditorHotKeyEnabled,
|
|
fetchSignatureFlagFromUISettings,
|
|
setQuotedReplyFlagForInbox,
|
|
fetchQuotedReplyFlagFromUISettings,
|
|
replyEditor,
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
message: '',
|
|
inReplyTo: {},
|
|
isFocused: false,
|
|
showEmojiPicker: false,
|
|
attachedFiles: [],
|
|
isRecordingAudio: false,
|
|
recordingAudioState: '',
|
|
recordingAudioDurationText: '',
|
|
isUploading: false,
|
|
replyType: REPLY_EDITOR_MODES.REPLY,
|
|
mentionSearchKey: '',
|
|
hasSlashCommand: false,
|
|
bccEmails: '',
|
|
ccEmails: '',
|
|
toEmails: '',
|
|
doAutoSaveDraft: () => {},
|
|
showWhatsAppTemplatesModal: false,
|
|
showContentTemplatesModal: false,
|
|
updateEditorSelectionWith: '',
|
|
undefinedVariableMessage: '',
|
|
showMentions: false,
|
|
showUserMentions: false,
|
|
showCannedMenu: false,
|
|
showVariablesMenu: false,
|
|
newConversationModalActive: false,
|
|
showArticleSearchPopover: false,
|
|
hasRecordedAudio: false,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters({
|
|
currentChat: 'getSelectedChat',
|
|
messageSignature: 'getMessageSignature',
|
|
currentUser: 'getCurrentUser',
|
|
lastEmail: 'getLastEmailInSelectedChat',
|
|
globalConfig: 'globalConfig/get',
|
|
accountId: 'getCurrentAccountId',
|
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
|
}),
|
|
currentContact() {
|
|
return this.$store.getters['contacts/getContact'](
|
|
this.currentChat.meta.sender.id
|
|
);
|
|
},
|
|
shouldShowReplyToMessage() {
|
|
return (
|
|
this.inReplyTo?.id &&
|
|
!this.isPrivate &&
|
|
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
|
|
!this.is360DialogWhatsAppChannel
|
|
);
|
|
},
|
|
showRichContentEditor() {
|
|
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
|
|
return true;
|
|
}
|
|
|
|
if (this.isAPIInbox) {
|
|
const {
|
|
display_rich_content_editor: displayRichContentEditor = false,
|
|
} = this.uiSettings;
|
|
return displayRichContentEditor;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
showWhatsappTemplates() {
|
|
return this.isAWhatsAppCloudChannel && !this.isPrivate;
|
|
},
|
|
showContentTemplates() {
|
|
return this.isATwilioWhatsAppChannel && !this.isPrivate;
|
|
},
|
|
isPrivate() {
|
|
if (this.currentChat.can_reply || this.isAWhatsAppChannel) {
|
|
return this.isOnPrivateNote;
|
|
}
|
|
return true;
|
|
},
|
|
isReplyRestricted() {
|
|
return !this.currentChat?.can_reply && !this.isAWhatsAppChannel;
|
|
},
|
|
inboxId() {
|
|
return this.currentChat.inbox_id;
|
|
},
|
|
inbox() {
|
|
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
|
},
|
|
messagePlaceHolder() {
|
|
return this.isPrivate
|
|
? this.$t('CONVERSATION.FOOTER.PRIVATE_MSG_INPUT')
|
|
: this.$t('CONVERSATION.FOOTER.MSG_INPUT');
|
|
},
|
|
isMessageLengthReachingThreshold() {
|
|
return this.message.length > this.maxLength - 50;
|
|
},
|
|
charactersRemaining() {
|
|
return this.maxLength - this.message.length;
|
|
},
|
|
isReplyButtonDisabled() {
|
|
if (this.isATwitterInbox) return true;
|
|
if (this.hasAttachments || this.hasRecordedAudio) return false;
|
|
|
|
return (
|
|
this.isMessageEmpty ||
|
|
this.message.length === 0 ||
|
|
this.message.length > this.maxLength
|
|
);
|
|
},
|
|
sender() {
|
|
return {
|
|
name: this.currentUser.name,
|
|
thumbnail: this.currentUser.avatar_url,
|
|
};
|
|
},
|
|
conversationType() {
|
|
const { additional_attributes: additionalAttributes } = this.currentChat;
|
|
const type = additionalAttributes ? additionalAttributes.type : '';
|
|
return type || '';
|
|
},
|
|
maxLength() {
|
|
if (this.isPrivate) {
|
|
return MESSAGE_MAX_LENGTH.GENERAL;
|
|
}
|
|
if (this.isAFacebookInbox) {
|
|
return MESSAGE_MAX_LENGTH.FACEBOOK;
|
|
}
|
|
if (this.isAnInstagramChannel) {
|
|
return MESSAGE_MAX_LENGTH.INSTAGRAM;
|
|
}
|
|
if (this.isATwilioWhatsAppChannel) {
|
|
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
|
}
|
|
if (this.isAWhatsAppCloudChannel) {
|
|
return MESSAGE_MAX_LENGTH.WHATSAPP_CLOUD;
|
|
}
|
|
if (this.isASmsInbox) {
|
|
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
|
}
|
|
if (this.isAnEmailChannel) {
|
|
return MESSAGE_MAX_LENGTH.EMAIL;
|
|
}
|
|
if (this.isATwilioSMSChannel) {
|
|
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
|
}
|
|
if (this.isAWhatsAppChannel) {
|
|
return MESSAGE_MAX_LENGTH.WHATSAPP_CLOUD;
|
|
}
|
|
return MESSAGE_MAX_LENGTH.GENERAL;
|
|
},
|
|
showFileUpload() {
|
|
return (
|
|
this.isAWebWidgetInbox ||
|
|
this.isAFacebookInbox ||
|
|
this.isAWhatsAppChannel ||
|
|
this.isAPIInbox ||
|
|
this.isAnEmailChannel ||
|
|
this.isASmsInbox ||
|
|
this.isATelegramChannel ||
|
|
this.isALineChannel ||
|
|
this.isAnInstagramChannel
|
|
);
|
|
},
|
|
replyButtonLabel() {
|
|
let sendMessageText = this.$t('CONVERSATION.REPLYBOX.SEND');
|
|
if (this.isPrivate) {
|
|
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
|
}
|
|
const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
|
|
? '(⌘ + ↵)'
|
|
: '(↵)';
|
|
return `${sendMessageText} ${keyLabel}`;
|
|
},
|
|
replyBoxClass() {
|
|
return {
|
|
'is-private': this.isPrivate,
|
|
'is-focused': this.isFocused || this.hasAttachments,
|
|
};
|
|
},
|
|
hasAttachments() {
|
|
return this.attachedFiles.length;
|
|
},
|
|
isRichEditorEnabled() {
|
|
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
|
},
|
|
showAudioRecorder() {
|
|
return !this.isOnPrivateNote && this.showFileUpload;
|
|
},
|
|
showAudioRecorderEditor() {
|
|
return this.showAudioRecorder && this.isRecordingAudio;
|
|
},
|
|
isOnPrivateNote() {
|
|
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
|
},
|
|
isOnExpandedLayout() {
|
|
const {
|
|
LAYOUT_TYPES: { CONDENSED },
|
|
} = wootConstants;
|
|
const { conversation_display_type: conversationDisplayType = CONDENSED } =
|
|
this.uiSettings;
|
|
return conversationDisplayType !== CONDENSED;
|
|
},
|
|
isMessageEmpty() {
|
|
if (!this.message) {
|
|
return true;
|
|
}
|
|
return !this.message.trim().replace(/\n/g, '').length;
|
|
},
|
|
showReplyHead() {
|
|
return !this.isOnPrivateNote && this.isAnEmailChannel;
|
|
},
|
|
enableMultipleFileUpload() {
|
|
return (
|
|
this.isAnEmailChannel ||
|
|
this.isAWebWidgetInbox ||
|
|
this.isAPIInbox ||
|
|
this.isAWhatsAppChannel ||
|
|
this.isATelegramChannel
|
|
);
|
|
},
|
|
isSignatureEnabledForInbox() {
|
|
return !this.isPrivate && this.sendWithSignature;
|
|
},
|
|
isSignatureAvailable() {
|
|
return !!this.signatureToApply;
|
|
},
|
|
sendWithSignature() {
|
|
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
|
},
|
|
editorMessageKey() {
|
|
const { editor_message_key: isEnabled } = this.uiSettings;
|
|
return isEnabled;
|
|
},
|
|
commandPlusEnterToSendEnabled() {
|
|
return this.editorMessageKey === 'cmd_enter';
|
|
},
|
|
enterToSendEnabled() {
|
|
return this.editorMessageKey === 'enter';
|
|
},
|
|
conversationId() {
|
|
return this.currentChat.id;
|
|
},
|
|
conversationIdByRoute() {
|
|
return this.conversationId;
|
|
},
|
|
editorStateId() {
|
|
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
|
},
|
|
audioRecordFormat() {
|
|
if (this.isAWhatsAppChannel || this.isATelegramChannel) {
|
|
return AUDIO_FORMATS.MP3;
|
|
}
|
|
if (this.isAPIInbox) {
|
|
return AUDIO_FORMATS.MP3;
|
|
}
|
|
return AUDIO_FORMATS.WAV;
|
|
},
|
|
messageVariables() {
|
|
const variables = getMessageVariables({
|
|
conversation: this.currentChat,
|
|
contact: this.currentContact,
|
|
inbox: this.inbox,
|
|
});
|
|
return variables;
|
|
},
|
|
// ensure that the signature is plain text depending on `showRichContentEditor`
|
|
signatureToApply() {
|
|
return this.showRichContentEditor
|
|
? this.messageSignature
|
|
: extractTextFromMarkdown(this.messageSignature);
|
|
},
|
|
connectedPortalSlug() {
|
|
const { help_center: portal = {} } = this.inbox;
|
|
const { slug = '' } = portal;
|
|
return slug;
|
|
},
|
|
isQuotedEmailReplyEnabled() {
|
|
return this.isFeatureEnabledonAccount(
|
|
this.accountId,
|
|
FEATURE_FLAGS.QUOTED_EMAIL_REPLY
|
|
);
|
|
},
|
|
quotedReplyPreference() {
|
|
if (!this.isAnEmailChannel || !this.isQuotedEmailReplyEnabled) {
|
|
return false;
|
|
}
|
|
|
|
return !!this.fetchQuotedReplyFlagFromUISettings(this.channelType);
|
|
},
|
|
lastEmailWithQuotedContent() {
|
|
if (!this.isAnEmailChannel) {
|
|
return null;
|
|
}
|
|
|
|
const lastEmail = this.lastEmail;
|
|
if (!lastEmail || lastEmail.private) {
|
|
return null;
|
|
}
|
|
|
|
return lastEmail;
|
|
},
|
|
quotedEmailText() {
|
|
return extractQuotedEmailText(this.lastEmailWithQuotedContent);
|
|
},
|
|
quotedEmailPreviewText() {
|
|
return truncatePreviewText(this.quotedEmailText, 80);
|
|
},
|
|
shouldShowQuotedReplyToggle() {
|
|
return (
|
|
this.isAnEmailChannel &&
|
|
!this.isOnPrivateNote &&
|
|
this.isQuotedEmailReplyEnabled
|
|
);
|
|
},
|
|
shouldShowQuotedPreview() {
|
|
return (
|
|
this.shouldShowQuotedReplyToggle &&
|
|
this.quotedReplyPreference &&
|
|
!!this.quotedEmailText
|
|
);
|
|
},
|
|
},
|
|
watch: {
|
|
currentChat(conversation, oldConversation) {
|
|
const { can_reply: canReply } = conversation;
|
|
if (oldConversation && oldConversation.id !== conversation.id) {
|
|
// Only update email fields when switching to a completely different conversation (by ID)
|
|
// This prevents overwriting user input (e.g., CC/BCC fields) when performing actions
|
|
// like self-assign or other updates that do not actually change the conversation context
|
|
this.setCCAndToEmailsFromLastChat();
|
|
}
|
|
|
|
if (this.isOnPrivateNote) {
|
|
return;
|
|
}
|
|
|
|
if (canReply || this.isAWhatsAppChannel) {
|
|
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
|
} else {
|
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
|
}
|
|
|
|
this.fetchAndSetReplyTo();
|
|
},
|
|
// When moving from one conversation to another, the store may not have the
|
|
// list of all the messages. A fetch is subsequently made to get the messages.
|
|
// This watcher handles two main cases:
|
|
// 1. When switching conversations and messages are fetched/updated, ensures CC/BCC fields are set from the latest OUTGOING/INCOMING email (not activity/private messages).
|
|
// 2. Fixes and issue where CC/BCC fields could be reset/lost after assignment/activity actions or message mutations that did not represent a true email context change.
|
|
lastEmail: {
|
|
handler(lastEmail) {
|
|
if (!lastEmail) return;
|
|
this.setCCAndToEmailsFromLastChat();
|
|
},
|
|
deep: true,
|
|
},
|
|
conversationIdByRoute(conversationId, oldConversationId) {
|
|
if (conversationId !== oldConversationId) {
|
|
this.setToDraft(oldConversationId, this.replyType);
|
|
this.getFromDraft();
|
|
this.resetRecorderAndClearAttachments();
|
|
}
|
|
},
|
|
message(updatedMessage) {
|
|
// Check if the message starts with a slash.
|
|
const bodyWithoutSignature = removeSignature(
|
|
updatedMessage,
|
|
this.signatureToApply
|
|
);
|
|
const startsWithSlash = bodyWithoutSignature.startsWith('/');
|
|
|
|
// Determine if the user is potentially typing a slash command.
|
|
// This is true if the message starts with a slash and the rich content editor is not active.
|
|
this.hasSlashCommand = startsWithSlash && !this.showRichContentEditor;
|
|
this.showMentions = this.hasSlashCommand;
|
|
|
|
// If a slash command is active, extract the command text after the slash.
|
|
// If not, reset the mentionSearchKey.
|
|
this.mentionSearchKey = this.hasSlashCommand
|
|
? bodyWithoutSignature.substring(1)
|
|
: '';
|
|
|
|
// Autosave the current message draft.
|
|
this.doAutoSaveDraft();
|
|
},
|
|
replyType(updatedReplyType, oldReplyType) {
|
|
this.setToDraft(this.conversationIdByRoute, oldReplyType);
|
|
this.getFromDraft();
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
this.getFromDraft();
|
|
// Don't use the keyboard listener mixin here as the events here are supposed to be
|
|
// working even if input/textarea is focussed.
|
|
document.addEventListener('paste', this.onPaste);
|
|
document.addEventListener('keydown', this.handleKeyEvents);
|
|
this.setCCAndToEmailsFromLastChat();
|
|
this.doAutoSaveDraft = debounce(
|
|
() => {
|
|
this.saveDraft(this.conversationIdByRoute, this.replyType);
|
|
},
|
|
500,
|
|
true
|
|
);
|
|
|
|
this.fetchAndSetReplyTo();
|
|
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
|
|
|
// A hacky fix to solve the drag and drop
|
|
// Is showing on top of new conversation modal drag and drop
|
|
// TODO need to find a better solution
|
|
emitter.on(
|
|
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
|
this.onNewConversationModalActive
|
|
);
|
|
emitter.on(BUS_EVENTS.INSERT_INTO_NORMAL_EDITOR, this.addIntoEditor);
|
|
},
|
|
unmounted() {
|
|
document.removeEventListener('paste', this.onPaste);
|
|
document.removeEventListener('keydown', this.handleKeyEvents);
|
|
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
|
emitter.off(BUS_EVENTS.INSERT_INTO_NORMAL_EDITOR, this.addIntoEditor);
|
|
emitter.off(
|
|
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
|
this.onNewConversationModalActive
|
|
);
|
|
},
|
|
methods: {
|
|
handleInsert(article) {
|
|
const { url, title } = article;
|
|
if (this.isRichEditorEnabled) {
|
|
// Removing empty lines from the title
|
|
const lines = title.split('\n');
|
|
const nonEmptyLines = lines.filter(line => line.trim() !== '');
|
|
const filteredMarkdown = nonEmptyLines.join(' ');
|
|
emitter.emit(
|
|
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
|
|
`[${filteredMarkdown}](${url})`
|
|
);
|
|
} else {
|
|
this.addIntoEditor(
|
|
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
|
|
);
|
|
}
|
|
|
|
useTrack(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
|
|
},
|
|
toggleRichContentEditor() {
|
|
this.updateUISettings({
|
|
display_rich_content_editor: !this.showRichContentEditor,
|
|
});
|
|
|
|
const plainTextSignature = extractTextFromMarkdown(this.messageSignature);
|
|
|
|
if (!this.showRichContentEditor && this.messageSignature) {
|
|
// remove the old signature -> extract text from markdown -> attach new signature
|
|
let message = removeSignature(this.message, this.messageSignature);
|
|
message = extractTextFromMarkdown(message);
|
|
message = appendSignature(message, plainTextSignature);
|
|
|
|
this.message = message;
|
|
} else {
|
|
this.message = replaceSignature(
|
|
this.message,
|
|
plainTextSignature,
|
|
this.messageSignature
|
|
);
|
|
}
|
|
},
|
|
toggleQuotedReply() {
|
|
if (!this.isAnEmailChannel) {
|
|
return;
|
|
}
|
|
|
|
const nextValue = !this.quotedReplyPreference;
|
|
this.setQuotedReplyFlagForInbox(this.channelType, nextValue);
|
|
},
|
|
shouldIncludeQuotedEmail() {
|
|
return (
|
|
this.isQuotedEmailReplyEnabled &&
|
|
this.quotedReplyPreference &&
|
|
this.shouldShowQuotedReplyToggle &&
|
|
!!this.quotedEmailText
|
|
);
|
|
},
|
|
getMessageWithQuotedEmailText(message) {
|
|
if (!this.shouldIncludeQuotedEmail()) {
|
|
return message;
|
|
}
|
|
|
|
const quotedText = this.quotedEmailText || '';
|
|
const header = buildQuotedEmailHeader(
|
|
this.lastEmailWithQuotedContent,
|
|
this.currentContact,
|
|
this.inbox
|
|
);
|
|
|
|
return appendQuotedTextToMessage(message, quotedText, header);
|
|
},
|
|
resetRecorderAndClearAttachments() {
|
|
// Reset audio recorder UI state
|
|
this.resetAudioRecorderInput();
|
|
// Reset attached files
|
|
this.attachedFiles = [];
|
|
},
|
|
saveDraft(conversationId, replyType) {
|
|
if (this.message || this.message === '') {
|
|
const key = `draft-${conversationId}-${replyType}`;
|
|
const draftToSave = trimContent(this.message || '');
|
|
|
|
this.$store.dispatch('draftMessages/set', {
|
|
key,
|
|
message: draftToSave,
|
|
});
|
|
}
|
|
},
|
|
setToDraft(conversationId, replyType) {
|
|
this.saveDraft(conversationId, replyType);
|
|
this.message = '';
|
|
},
|
|
getFromDraft() {
|
|
if (this.conversationIdByRoute) {
|
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
|
const messageFromStore =
|
|
this.$store.getters['draftMessages/get'](key) || '';
|
|
|
|
// ensure that the message has signature set based on the ui setting
|
|
this.message = this.toggleSignatureForDraft(messageFromStore);
|
|
}
|
|
},
|
|
toggleSignatureForDraft(message) {
|
|
if (this.isPrivate) {
|
|
return message;
|
|
}
|
|
|
|
return this.sendWithSignature
|
|
? appendSignature(message, this.signatureToApply)
|
|
: removeSignature(message, this.signatureToApply);
|
|
},
|
|
removeFromDraft() {
|
|
if (this.conversationIdByRoute) {
|
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
|
this.$store.dispatch('draftMessages/delete', { key });
|
|
}
|
|
},
|
|
getElementToBind() {
|
|
return this.replyEditor;
|
|
},
|
|
getKeyboardEvents() {
|
|
return {
|
|
Escape: {
|
|
action: () => {
|
|
this.hideEmojiPicker();
|
|
this.hideMentions();
|
|
},
|
|
allowOnFocusedInput: true,
|
|
},
|
|
'$mod+KeyK': {
|
|
action: e => {
|
|
e.preventDefault();
|
|
const ninja = document.querySelector('ninja-keys');
|
|
ninja.open();
|
|
},
|
|
allowOnFocusedInput: true,
|
|
},
|
|
Enter: {
|
|
action: e => {
|
|
if (this.isAValidEvent('enter')) {
|
|
this.onSendReply();
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
allowOnFocusedInput: true,
|
|
},
|
|
'$mod+Enter': {
|
|
action: () => {
|
|
if (this.isAValidEvent('cmd_enter')) {
|
|
this.onSendReply();
|
|
}
|
|
},
|
|
allowOnFocusedInput: true,
|
|
},
|
|
};
|
|
},
|
|
isAValidEvent(selectedKey) {
|
|
return (
|
|
!this.showUserMentions &&
|
|
!this.showMentions &&
|
|
!this.showCannedMenu &&
|
|
!this.showVariablesMenu &&
|
|
this.isFocused &&
|
|
this.isEditorHotKeyEnabled(selectedKey)
|
|
);
|
|
},
|
|
onPaste(e) {
|
|
const data = e.clipboardData.files;
|
|
if (!this.showRichContentEditor && data.length !== 0) {
|
|
this.$refs.messageInput.$el.blur();
|
|
}
|
|
if (!data.length || !data[0]) {
|
|
return;
|
|
}
|
|
data.forEach(file => {
|
|
const { name, type, size } = file;
|
|
this.onFileUpload({ name, type, size, file: file });
|
|
});
|
|
},
|
|
toggleUserMention(currentMentionState) {
|
|
this.showUserMentions = currentMentionState;
|
|
},
|
|
toggleCannedMenu(value) {
|
|
this.showCannedMenu = value;
|
|
},
|
|
toggleVariablesMenu(value) {
|
|
this.showVariablesMenu = value;
|
|
},
|
|
openWhatsappTemplateModal() {
|
|
this.showWhatsAppTemplatesModal = true;
|
|
},
|
|
hideWhatsappTemplatesModal() {
|
|
this.showWhatsAppTemplatesModal = false;
|
|
},
|
|
openContentTemplateModal() {
|
|
this.showContentTemplatesModal = true;
|
|
},
|
|
hideContentTemplatesModal() {
|
|
this.showContentTemplatesModal = false;
|
|
},
|
|
confirmOnSendReply() {
|
|
if (this.isReplyButtonDisabled) {
|
|
return;
|
|
}
|
|
if (!this.showMentions) {
|
|
const isOnWhatsApp =
|
|
this.isATwilioWhatsAppChannel ||
|
|
this.isAWhatsAppCloudChannel ||
|
|
this.is360DialogWhatsAppChannel;
|
|
// When users send messages containing both text and attachments on Instagram, Instagram treats them as separate messages.
|
|
// Although Chatwoot combines these into a single message, Instagram sends separate echo events for each component.
|
|
// This can create duplicate messages in Chatwoot. To prevent this issue, we'll handle text and attachments as separate messages.
|
|
const isOnInstagram = this.isAnInstagramChannel;
|
|
if ((isOnWhatsApp || isOnInstagram) && !this.isPrivate) {
|
|
this.sendMessageAsMultipleMessages(this.message);
|
|
} else {
|
|
const messagePayload = this.getMessagePayload(this.message);
|
|
this.sendMessage(messagePayload);
|
|
}
|
|
|
|
if (!this.isPrivate) {
|
|
this.clearEmailField();
|
|
}
|
|
|
|
this.clearMessage();
|
|
this.hideEmojiPicker();
|
|
this.$emit('update:popOutReplyBox', false);
|
|
}
|
|
},
|
|
sendMessageAsMultipleMessages(message) {
|
|
const messages = this.getMultipleMessagesPayload(message);
|
|
messages.forEach(messagePayload => {
|
|
this.sendMessage(messagePayload);
|
|
});
|
|
},
|
|
sendMessageAnalyticsData(isPrivate) {
|
|
// Analytics data for message signature is enabled or not in channels
|
|
return isPrivate
|
|
? useTrack(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
|
|
: useTrack(CONVERSATION_EVENTS.SENT_MESSAGE, {
|
|
channelType: this.channelType,
|
|
signatureEnabled: this.sendWithSignature,
|
|
hasReplyTo: !!this.inReplyTo?.id,
|
|
});
|
|
},
|
|
async onSendReply() {
|
|
const undefinedVariables = getUndefinedVariablesInMessage({
|
|
message: this.message,
|
|
variables: this.messageVariables,
|
|
});
|
|
if (undefinedVariables.length > 0) {
|
|
const undefinedVariablesCount =
|
|
undefinedVariables.length > 1 ? undefinedVariables.length : 1;
|
|
this.undefinedVariableMessage = this.$t(
|
|
'CONVERSATION.REPLYBOX.UNDEFINED_VARIABLES.MESSAGE',
|
|
{
|
|
undefinedVariablesCount,
|
|
undefinedVariables: undefinedVariables.join(', '),
|
|
}
|
|
);
|
|
|
|
const ok = await this.$refs.confirmDialog.showConfirmation();
|
|
if (ok) {
|
|
this.confirmOnSendReply();
|
|
}
|
|
} else {
|
|
this.confirmOnSendReply();
|
|
}
|
|
},
|
|
async sendMessage(messagePayload) {
|
|
try {
|
|
await this.$store.dispatch(
|
|
'createPendingMessageAndSend',
|
|
messagePayload
|
|
);
|
|
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
|
emitter.emit(BUS_EVENTS.MESSAGE_SENT);
|
|
this.removeFromDraft();
|
|
this.sendMessageAnalyticsData(messagePayload.private);
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
|
useAlert(errorMessage);
|
|
}
|
|
},
|
|
async onSendWhatsAppReply(messagePayload) {
|
|
this.sendMessage({
|
|
conversationId: this.currentChat.id,
|
|
...messagePayload,
|
|
});
|
|
this.hideWhatsappTemplatesModal();
|
|
},
|
|
async onSendContentTemplateReply(messagePayload) {
|
|
this.sendMessage({
|
|
conversationId: this.currentChat.id,
|
|
...messagePayload,
|
|
});
|
|
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
|
|
message = appendSignature(message, this.signatureToApply);
|
|
}
|
|
|
|
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
|
|
if (this.attachedFiles.length > 0) this.attachedFiles = [];
|
|
|
|
const { can_reply: canReply } = this.currentChat;
|
|
this.$store.dispatch('draftMessages/setReplyEditorMode', {
|
|
mode,
|
|
});
|
|
if (canReply || this.isAWhatsAppChannel) this.replyType = mode;
|
|
if (this.showRichContentEditor) {
|
|
if (this.isRecordingAudio) {
|
|
this.toggleAudioRecorder();
|
|
}
|
|
return;
|
|
}
|
|
this.$nextTick(() => this.$refs.messageInput.focus());
|
|
},
|
|
clearEditorSelection() {
|
|
this.updateEditorSelectionWith = '';
|
|
},
|
|
insertIntoTextEditor(text, selectionStart, selectionEnd) {
|
|
const { message } = this;
|
|
const newMessage =
|
|
message.slice(0, selectionStart) +
|
|
text +
|
|
message.slice(selectionEnd, message.length);
|
|
this.message = newMessage;
|
|
},
|
|
addIntoEditor(content) {
|
|
if (this.showRichContentEditor) {
|
|
this.updateEditorSelectionWith = content;
|
|
this.onFocus();
|
|
}
|
|
if (!this.showRichContentEditor) {
|
|
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
|
|
this.insertIntoTextEditor(content, selectionStart, selectionEnd);
|
|
}
|
|
},
|
|
clearMessage() {
|
|
this.message = '';
|
|
if (this.sendWithSignature && !this.isPrivate) {
|
|
// if signature is enabled, append it to the message
|
|
this.message = appendSignature(this.message, this.signatureToApply);
|
|
}
|
|
this.attachedFiles = [];
|
|
this.isRecordingAudio = false;
|
|
this.resetReplyToMessage();
|
|
this.resetAudioRecorderInput();
|
|
},
|
|
clearEmailField() {
|
|
this.ccEmails = '';
|
|
this.bccEmails = '';
|
|
this.toEmails = '';
|
|
},
|
|
|
|
toggleEmojiPicker() {
|
|
this.showEmojiPicker = !this.showEmojiPicker;
|
|
},
|
|
toggleAudioRecorder() {
|
|
this.isRecordingAudio = !this.isRecordingAudio;
|
|
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
|
if (!this.isRecordingAudio) {
|
|
this.resetAudioRecorderInput();
|
|
}
|
|
},
|
|
toggleAudioRecorderPlayPause() {
|
|
if (!this.isRecordingAudio) {
|
|
return;
|
|
}
|
|
if (!this.isRecorderAudioStopped) {
|
|
this.isRecorderAudioStopped = true;
|
|
this.$refs.audioRecorderInput.stopRecording();
|
|
} else if (this.isRecorderAudioStopped) {
|
|
this.$refs.audioRecorderInput.playPause();
|
|
}
|
|
},
|
|
hideEmojiPicker() {
|
|
if (this.showEmojiPicker) {
|
|
this.toggleEmojiPicker();
|
|
}
|
|
},
|
|
hideMentions() {
|
|
this.showMentions = false;
|
|
},
|
|
onTypingOn() {
|
|
this.toggleTyping('on');
|
|
},
|
|
onTypingOff() {
|
|
this.toggleTyping('off');
|
|
},
|
|
onBlur() {
|
|
this.isFocused = false;
|
|
this.saveDraft(this.conversationIdByRoute, this.replyType);
|
|
},
|
|
onFocus() {
|
|
this.isFocused = true;
|
|
},
|
|
onRecordProgressChanged(duration) {
|
|
this.recordingAudioDurationText = duration;
|
|
},
|
|
onFinishRecorder(file) {
|
|
this.recordingAudioState = 'stopped';
|
|
this.hasRecordedAudio = true;
|
|
// Added a new key isRecordedAudio to the file to find it's and recorded audio
|
|
// Because to filter and show only non recorded audio and other attachments
|
|
const autoRecordedFile = {
|
|
...file,
|
|
isRecordedAudio: true,
|
|
};
|
|
return file && this.onFileUpload(autoRecordedFile);
|
|
},
|
|
toggleTyping(status) {
|
|
const conversationId = this.currentChat.id;
|
|
const isPrivate = this.isPrivate;
|
|
|
|
if (!conversationId) {
|
|
return;
|
|
}
|
|
|
|
this.$store.dispatch('conversationTypingStatus/toggleTyping', {
|
|
status,
|
|
conversationId,
|
|
isPrivate,
|
|
});
|
|
},
|
|
attachFile({ blob, file }) {
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file.file);
|
|
reader.onloadend = () => {
|
|
this.attachedFiles.push({
|
|
currentChatId: this.currentChat.id,
|
|
resource: blob || file,
|
|
isPrivate: this.isPrivate,
|
|
thumb: reader.result,
|
|
blobSignedId: blob ? blob.signed_id : undefined,
|
|
isRecordedAudio: file?.isRecordedAudio || false,
|
|
});
|
|
};
|
|
},
|
|
removeAttachment(attachments) {
|
|
this.attachedFiles = attachments;
|
|
},
|
|
setReplyToInPayload(payload) {
|
|
if (this.inReplyTo?.id) {
|
|
return {
|
|
...payload,
|
|
contentAttributes: {
|
|
...payload.contentAttributes,
|
|
in_reply_to: this.inReplyTo.id,
|
|
},
|
|
};
|
|
}
|
|
|
|
return payload;
|
|
},
|
|
getMultipleMessagesPayload(message) {
|
|
const multipleMessagePayload = [];
|
|
|
|
if (this.attachedFiles && this.attachedFiles.length) {
|
|
let caption = this.isAnInstagramChannel ? '' : message;
|
|
this.attachedFiles.forEach(attachment => {
|
|
const attachedFile = this.globalConfig.directUploadsEnabled
|
|
? attachment.blobSignedId
|
|
: attachment.resource.file;
|
|
let attachmentPayload = {
|
|
conversationId: this.currentChat.id,
|
|
files: [attachedFile],
|
|
private: false,
|
|
message: caption,
|
|
sender: this.sender,
|
|
};
|
|
|
|
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
|
|
multipleMessagePayload.push(attachmentPayload);
|
|
// For WhatsApp, only the first attachment gets a caption
|
|
if (!this.isAnInstagramChannel) caption = '';
|
|
});
|
|
}
|
|
|
|
const hasNoAttachments =
|
|
!this.attachedFiles || !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 (
|
|
(this.isAnInstagramChannel && this.message) ||
|
|
(!this.isAnInstagramChannel && hasNoAttachments)
|
|
) {
|
|
let messagePayload = {
|
|
conversationId: this.currentChat.id,
|
|
message,
|
|
private: false,
|
|
sender: this.sender,
|
|
};
|
|
|
|
messagePayload = this.setReplyToInPayload(messagePayload);
|
|
|
|
multipleMessagePayload.push(messagePayload);
|
|
}
|
|
|
|
return multipleMessagePayload;
|
|
},
|
|
getMessagePayload(message) {
|
|
const messageWithQuote = this.getMessageWithQuotedEmailText(message);
|
|
|
|
let messagePayload = {
|
|
conversationId: this.currentChat.id,
|
|
message: messageWithQuote,
|
|
private: this.isPrivate,
|
|
sender: this.sender,
|
|
};
|
|
messagePayload = this.setReplyToInPayload(messagePayload);
|
|
|
|
if (this.attachedFiles && this.attachedFiles.length) {
|
|
messagePayload.files = [];
|
|
this.attachedFiles.forEach(attachment => {
|
|
if (this.globalConfig.directUploadsEnabled) {
|
|
messagePayload.files.push(attachment.blobSignedId);
|
|
} else {
|
|
messagePayload.files.push(attachment.resource.file);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (this.ccEmails && !this.isOnPrivateNote) {
|
|
messagePayload.ccEmails = this.ccEmails;
|
|
}
|
|
|
|
if (this.bccEmails && !this.isOnPrivateNote) {
|
|
messagePayload.bccEmails = this.bccEmails;
|
|
}
|
|
|
|
if (this.toEmails && !this.isOnPrivateNote) {
|
|
messagePayload.toEmails = this.toEmails;
|
|
}
|
|
return messagePayload;
|
|
},
|
|
setCcEmails(value) {
|
|
this.bccEmails = value.bccEmails;
|
|
this.ccEmails = value.ccEmails;
|
|
},
|
|
setCCAndToEmailsFromLastChat() {
|
|
const conversationContact = this.currentChat?.meta?.sender?.email || '';
|
|
const { email: inboxEmail, forward_to_email: forwardToEmail } =
|
|
this.inbox;
|
|
|
|
const { cc, bcc, to } = getRecipients(
|
|
this.lastEmail,
|
|
conversationContact,
|
|
inboxEmail,
|
|
forwardToEmail
|
|
);
|
|
|
|
this.toEmails = to.join(', ');
|
|
this.ccEmails = cc.join(', ');
|
|
this.bccEmails = bcc.join(', ');
|
|
},
|
|
fetchAndSetReplyTo() {
|
|
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
|
const replyToMessageId = LocalStorage.getFromJsonStore(
|
|
replyStorageKey,
|
|
this.conversationId
|
|
);
|
|
|
|
this.inReplyTo = this.currentChat?.messages?.find(message => {
|
|
if (message.id === replyToMessageId) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
},
|
|
resetReplyToMessage() {
|
|
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
|
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
|
|
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
|
|
},
|
|
onNewConversationModalActive(isActive) {
|
|
// Issue is if the new conversation modal is open and we drag and drop the file
|
|
// then the file is not getting attached to the new conversation modal
|
|
// and it is getting attached to the current conversation reply box
|
|
// so to fix this we are removing the drag and drop event listener from the current conversation reply box
|
|
// When new conversation modal is open
|
|
this.newConversationModalActive = isActive;
|
|
},
|
|
onSearchPopoverClose() {
|
|
this.showArticleSearchPopover = false;
|
|
},
|
|
toggleInsertArticle() {
|
|
this.showArticleSearchPopover = !this.showArticleSearchPopover;
|
|
},
|
|
resetAudioRecorderInput() {
|
|
this.recordingAudioDurationText = '00:00';
|
|
this.isRecordingAudio = false;
|
|
this.recordingAudioState = '';
|
|
this.hasRecordedAudio = false;
|
|
// Only clear the recorded audio when we click toggle button.
|
|
this.attachedFiles = this.attachedFiles.filter(
|
|
file => !file?.isRecordedAudio
|
|
);
|
|
},
|
|
togglePopout() {
|
|
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<ReplyBoxBanner :message="message" :is-on-private-note="isOnPrivateNote" />
|
|
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
|
|
<ReplyTopPanel
|
|
:mode="replyType"
|
|
:is-reply-restricted="isReplyRestricted"
|
|
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
|
:characters-remaining="charactersRemaining"
|
|
:popout-reply-box="popOutReplyBox"
|
|
@set-reply-mode="setReplyMode"
|
|
@toggle-popout="togglePopout"
|
|
/>
|
|
<ArticleSearchPopover
|
|
v-if="showArticleSearchPopover && connectedPortalSlug"
|
|
:selected-portal-slug="connectedPortalSlug"
|
|
@insert="handleInsert"
|
|
@close="onSearchPopoverClose"
|
|
/>
|
|
<div class="reply-box__top">
|
|
<ReplyToMessage
|
|
v-if="shouldShowReplyToMessage"
|
|
:message="inReplyTo"
|
|
@dismiss="resetReplyToMessage"
|
|
/>
|
|
<CannedResponse
|
|
v-if="showMentions && hasSlashCommand"
|
|
v-on-clickaway="hideMentions"
|
|
class="normal-editor__canned-box"
|
|
:search-key="mentionSearchKey"
|
|
@replace="replaceText"
|
|
/>
|
|
<EmojiInput
|
|
v-if="showEmojiPicker"
|
|
v-on-clickaway="hideEmojiPicker"
|
|
:class="{
|
|
'emoji-dialog--expanded': isOnExpandedLayout || popOutReplyBox,
|
|
}"
|
|
:on-click="addIntoEditor"
|
|
/>
|
|
<ReplyEmailHead
|
|
v-if="showReplyHead"
|
|
v-model:cc-emails="ccEmails"
|
|
v-model:bcc-emails="bccEmails"
|
|
v-model:to-emails="toEmails"
|
|
/>
|
|
<AudioRecorder
|
|
v-if="showAudioRecorderEditor"
|
|
ref="audioRecorderInput"
|
|
:audio-record-format="audioRecordFormat"
|
|
@recorder-progress-changed="onRecordProgressChanged"
|
|
@finish-record="onFinishRecorder"
|
|
@play="recordingAudioState = 'playing'"
|
|
@pause="recordingAudioState = 'paused'"
|
|
/>
|
|
<ResizableTextArea
|
|
v-else-if="!showRichContentEditor"
|
|
ref="messageInput"
|
|
v-model="message"
|
|
class="rounded-none input"
|
|
:placeholder="messagePlaceHolder"
|
|
:min-height="4"
|
|
:signature="signatureToApply"
|
|
allow-signature
|
|
:send-with-signature="sendWithSignature"
|
|
@typing-off="onTypingOff"
|
|
@typing-on="onTypingOn"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
/>
|
|
<WootMessageEditor
|
|
v-else
|
|
v-model="message"
|
|
:editor-id="editorStateId"
|
|
class="input"
|
|
:is-private="isOnPrivateNote"
|
|
:placeholder="messagePlaceHolder"
|
|
:update-selection-with="updateEditorSelectionWith"
|
|
:min-height="4"
|
|
enable-variables
|
|
:variables="messageVariables"
|
|
:signature="signatureToApply"
|
|
allow-signature
|
|
:channel-type="channelType"
|
|
@typing-off="onTypingOff"
|
|
@typing-on="onTypingOn"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
@toggle-user-mention="toggleUserMention"
|
|
@toggle-canned-menu="toggleCannedMenu"
|
|
@toggle-variables-menu="toggleVariablesMenu"
|
|
@clear-selection="clearEditorSelection"
|
|
/>
|
|
<QuotedEmailPreview
|
|
v-if="shouldShowQuotedPreview"
|
|
:quoted-email-text="quotedEmailText"
|
|
:preview-text="quotedEmailPreviewText"
|
|
@toggle="toggleQuotedReply"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-if="hasAttachments && !showAudioRecorderEditor"
|
|
class="attachment-preview-box"
|
|
@paste="onPaste"
|
|
>
|
|
<AttachmentPreview
|
|
class="flex-col mt-4"
|
|
:attachments="attachedFiles"
|
|
@remove-attachment="removeAttachment"
|
|
/>
|
|
</div>
|
|
<MessageSignatureMissingAlert
|
|
v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
|
|
/>
|
|
<ReplyBottomPanel
|
|
:conversation-id="conversationId"
|
|
:enable-multiple-file-upload="enableMultipleFileUpload"
|
|
:enable-whats-app-templates="showWhatsappTemplates"
|
|
:enable-content-templates="showContentTemplates"
|
|
:inbox="inbox"
|
|
:is-on-private-note="isOnPrivateNote"
|
|
:is-recording-audio="isRecordingAudio"
|
|
:is-send-disabled="isReplyButtonDisabled"
|
|
:is-note="isPrivate"
|
|
:on-file-upload="onFileUpload"
|
|
:on-send="onSendReply"
|
|
:conversation-type="conversationType"
|
|
:recording-audio-duration-text="recordingAudioDurationText"
|
|
:recording-audio-state="recordingAudioState"
|
|
:send-button-text="replyButtonLabel"
|
|
:show-audio-recorder="showAudioRecorder"
|
|
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
|
|
:show-emoji-picker="showEmojiPicker"
|
|
:show-file-upload="showFileUpload"
|
|
:show-quoted-reply-toggle="shouldShowQuotedReplyToggle"
|
|
:quoted-reply-enabled="quotedReplyPreference"
|
|
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
|
:toggle-audio-recorder="toggleAudioRecorder"
|
|
:toggle-emoji-picker="toggleEmojiPicker"
|
|
:message="message"
|
|
:portal-slug="connectedPortalSlug"
|
|
:new-conversation-modal-active="newConversationModalActive"
|
|
@select-whatsapp-template="openWhatsappTemplateModal"
|
|
@select-content-template="openContentTemplateModal"
|
|
@toggle-editor="toggleRichContentEditor"
|
|
@replace-text="replaceText"
|
|
@toggle-insert-article="toggleInsertArticle"
|
|
@toggle-quoted-reply="toggleQuotedReply"
|
|
/>
|
|
<WhatsappTemplates
|
|
:inbox-id="inbox.id"
|
|
:show="showWhatsAppTemplatesModal"
|
|
@close="hideWhatsappTemplatesModal"
|
|
@on-send="onSendWhatsAppReply"
|
|
@cancel="hideWhatsappTemplatesModal"
|
|
/>
|
|
|
|
<ContentTemplates
|
|
:inbox-id="inbox.id"
|
|
:show="showContentTemplatesModal"
|
|
@close="hideContentTemplatesModal"
|
|
@on-send="onSendContentTemplateReply"
|
|
@cancel="hideContentTemplatesModal"
|
|
/>
|
|
|
|
<woot-confirm-modal
|
|
ref="confirmDialog"
|
|
:title="$t('CONVERSATION.REPLYBOX.UNDEFINED_VARIABLES.TITLE')"
|
|
:description="undefinedVariableMessage"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.send-button {
|
|
@apply mb-0;
|
|
}
|
|
|
|
.attachment-preview-box {
|
|
@apply bg-transparent py-0 px-4;
|
|
}
|
|
|
|
.reply-box {
|
|
transition: height 2s cubic-bezier(0.37, 0, 0.63, 1);
|
|
|
|
@apply relative mb-2 mx-2 border border-n-weak rounded-xl bg-n-solid-1;
|
|
|
|
&.is-private {
|
|
@apply bg-n-solid-amber dark:border-n-amber-3/10 border-n-amber-12/5;
|
|
}
|
|
}
|
|
|
|
.send-button {
|
|
@apply mb-0;
|
|
}
|
|
|
|
.reply-box__top {
|
|
@apply relative py-0 px-4 -mt-px;
|
|
|
|
textarea {
|
|
@apply shadow-none outline-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
|
|
}
|
|
}
|
|
|
|
.emoji-dialog {
|
|
@apply top-[unset] -bottom-10 ltr:-left-80 ltr:right-[unset] rtl:left-[unset] rtl:-right-80;
|
|
|
|
&::before {
|
|
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
|
@apply ltr:-right-4 bottom-2 rtl:-left-4 ltr:rotate-[270deg] rtl:rotate-[90deg];
|
|
}
|
|
}
|
|
|
|
.emoji-dialog--expanded {
|
|
@apply left-[unset] bottom-0 absolute z-[100];
|
|
|
|
&::before {
|
|
transform: rotate(0deg);
|
|
@apply ltr:left-1 rtl:right-1 -bottom-2;
|
|
}
|
|
}
|
|
|
|
.normal-editor__canned-box {
|
|
width: calc(100% - 2 * 1rem);
|
|
left: 1rem;
|
|
}
|
|
</style>
|