The ReplyBox multi-attachment split only covered Twilio, Cloud, and 360Dialog providers. Baileys and Z-API were added later and were missing from the check, causing extra attachments to be silently dropped. Use the generic isAWhatsAppChannel flag instead.
1647 lines
53 KiB
Vue
1647 lines
53 KiB
Vue
<script>
|
|
import { defineAsyncComponent, useTemplateRef } from 'vue';
|
|
import { mapGetters } from 'vuex';
|
|
import { useAlert } from 'dashboard/composables';
|
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
import { useInboxSignatures } from 'dashboard/composables/useInboxSignatures';
|
|
import { useTrack } from 'dashboard/composables';
|
|
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
|
|
import ReplyToMessage from './ReplyToMessage.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 CopilotReplyBottomPanel from 'dashboard/components/widgets/WootWriter/CopilotReplyBottomPanel.vue';
|
|
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
|
import CopilotEditorSection from './CopilotEditorSection.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 { 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';
|
|
import ScheduledMessageModal from 'dashboard/routes/dashboard/conversation/scheduledMessages/ScheduledMessageModal.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,
|
|
CAPTAIN_EVENTS,
|
|
} from '../../../helper/AnalyticsHelper/events';
|
|
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
|
import { appendSignature } from 'dashboard/helper/editorHelper';
|
|
import { useCopilotReply } from 'dashboard/composables/useCopilotReply';
|
|
import { useKbd } from 'dashboard/composables/utils/useKbd';
|
|
import { isFileTypeAllowedForChannel } from 'shared/helpers/FileHelper';
|
|
import { isInboxAdminInGroup } from 'dashboard/helper/phoneHelper';
|
|
|
|
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,
|
|
ReplyBoxBanner,
|
|
EmojiInput,
|
|
MessageSignatureMissingAlert,
|
|
ReplyBottomPanel,
|
|
ReplyEmailHead,
|
|
ReplyToMessage,
|
|
ReplyTopPanel,
|
|
ContentTemplates,
|
|
WhatsappTemplates,
|
|
WootMessageEditor,
|
|
QuotedEmailPreview,
|
|
CopilotEditorSection,
|
|
CopilotReplyBottomPanel,
|
|
ScheduledMessageModal,
|
|
},
|
|
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
|
props: {
|
|
popOutReplyBox: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
emits: ['update:popOutReplyBox'],
|
|
setup() {
|
|
const {
|
|
uiSettings,
|
|
isEditorHotKeyEnabled,
|
|
fetchSignatureFlagFromUISettings,
|
|
setQuotedReplyFlagForInbox,
|
|
fetchQuotedReplyFlagFromUISettings,
|
|
} = useUISettings();
|
|
|
|
const {
|
|
fetchInboxSignatures,
|
|
getSignatureForInbox,
|
|
getSignatureSettingsForInbox,
|
|
} = useInboxSignatures();
|
|
|
|
fetchInboxSignatures();
|
|
|
|
const { formatMessage } = useMessageFormatter();
|
|
|
|
const replyEditor = useTemplateRef('replyEditor');
|
|
const copilot = useCopilotReply();
|
|
const shortcutKey = useKbd(['$mod', '+', 'enter']);
|
|
|
|
return {
|
|
uiSettings,
|
|
isEditorHotKeyEnabled,
|
|
fetchSignatureFlagFromUISettings,
|
|
setQuotedReplyFlagForInbox,
|
|
fetchQuotedReplyFlagFromUISettings,
|
|
getSignatureForInbox,
|
|
getSignatureSettingsForInbox,
|
|
replyEditor,
|
|
copilot,
|
|
shortcutKey,
|
|
formatMessage,
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
message: '',
|
|
inReplyTo: {},
|
|
isFocused: false,
|
|
showEmojiPicker: false,
|
|
attachedFiles: [],
|
|
isRecordingAudio: false,
|
|
recordingAudioState: '',
|
|
recordingAudioDurationText: '',
|
|
replyType: REPLY_EDITOR_MODES.REPLY,
|
|
bccEmails: '',
|
|
ccEmails: '',
|
|
toEmails: '',
|
|
doAutoSaveDraft: () => {},
|
|
showWhatsAppTemplatesModal: false,
|
|
showContentTemplatesModal: false,
|
|
updateEditorSelectionWith: '',
|
|
undefinedVariableMessage: '',
|
|
showMentions: false,
|
|
showUserMentions: false,
|
|
showCannedMenu: false,
|
|
showVariablesMenu: false,
|
|
newConversationModalActive: false,
|
|
showArticleSearchPopover: false,
|
|
showScheduledMessageModal: false,
|
|
copilotAcceptedMessages: {},
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters({
|
|
currentChat: 'getSelectedChat',
|
|
currentUser: 'getCurrentUser',
|
|
lastEmail: 'getLastEmailInSelectedChat',
|
|
globalConfig: 'globalConfig/get',
|
|
accountId: 'getCurrentAccountId',
|
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
|
}),
|
|
currentContact() {
|
|
const senderId = this.currentChat?.meta?.sender?.id;
|
|
if (!senderId) return {};
|
|
return this.$store.getters['contacts/getContact'](senderId);
|
|
},
|
|
isGroupConversation() {
|
|
return this.currentChat?.group_type === 'group';
|
|
},
|
|
groupContactId() {
|
|
return this.currentChat?.meta?.sender?.id || null;
|
|
},
|
|
inboxPhoneNumber() {
|
|
return this.inbox?.phone_number || null;
|
|
},
|
|
groupMembers() {
|
|
if (!this.groupContactId) return [];
|
|
return (
|
|
this.$store.getters['groupMembers/getGroupMembers'](
|
|
this.groupContactId
|
|
) || []
|
|
);
|
|
},
|
|
groupMembersMeta() {
|
|
if (!this.groupContactId) return {};
|
|
return (
|
|
this.$store.getters['groupMembers/getGroupMembersMeta'](
|
|
this.groupContactId
|
|
) || {}
|
|
);
|
|
},
|
|
isInboxAdminInCurrentGroup() {
|
|
const meta = this.groupMembersMeta;
|
|
if (meta.is_inbox_admin != null) return meta.is_inbox_admin;
|
|
const inboxPhone = meta.inbox_phone_number || this.inboxPhoneNumber;
|
|
return isInboxAdminInGroup(inboxPhone, this.groupMembers);
|
|
},
|
|
isGroupMembersLoaded() {
|
|
const meta = this.groupMembersMeta;
|
|
return meta.is_inbox_admin != null || this.groupMembers.length > 0;
|
|
},
|
|
isAnnouncementModeRestricted() {
|
|
return (
|
|
this.isAWhatsAppBaileysChannel &&
|
|
this.isGroupConversation &&
|
|
this.currentContact?.additional_attributes?.announce === true &&
|
|
this.isGroupMembersLoaded &&
|
|
!this.isInboxAdminInCurrentGroup
|
|
);
|
|
},
|
|
isGroupLeft() {
|
|
return (
|
|
this.isAWhatsAppBaileysChannel &&
|
|
this.isGroupConversation &&
|
|
this.currentContact?.additional_attributes?.group_left === true
|
|
);
|
|
},
|
|
isGroupsDisabled() {
|
|
return (
|
|
this.isAWhatsAppBaileysChannel &&
|
|
this.isGroupConversation &&
|
|
!this.globalConfig.baileysWhatsappGroupsEnabled
|
|
);
|
|
},
|
|
shouldShowReplyToMessage() {
|
|
return (
|
|
this.inReplyTo?.id &&
|
|
!this.isPrivate &&
|
|
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
|
|
!this.is360DialogWhatsAppChannel &&
|
|
!this.copilot.isActive.value
|
|
);
|
|
},
|
|
showWhatsappTemplates() {
|
|
// We support templates for API channels if someone updates templates manually via API
|
|
// That's why we don't explicitly check for channel type here
|
|
const templates = this.$store.getters['inboxes/getWhatsAppTemplates'](
|
|
this.inboxId
|
|
);
|
|
return !!(templates && templates.length) && !this.isPrivate;
|
|
},
|
|
showContentTemplates() {
|
|
return this.isATwilioWhatsAppChannel && !this.isPrivate;
|
|
},
|
|
isPrivate() {
|
|
if (
|
|
this.currentChat.can_reply ||
|
|
this.isAWhatsAppChannel ||
|
|
this.isAPIInbox
|
|
) {
|
|
return this.isOnPrivateNote;
|
|
}
|
|
return true;
|
|
},
|
|
isReplyRestricted() {
|
|
return (
|
|
!this.currentChat?.can_reply &&
|
|
!(this.isAWhatsAppChannel || this.isAPIInbox)
|
|
);
|
|
},
|
|
inboxId() {
|
|
return this.currentChat.inbox_id;
|
|
},
|
|
inbox() {
|
|
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
|
},
|
|
messagePlaceHolder() {
|
|
if (this.isGroupsDisabled && !this.isOnPrivateNote) {
|
|
return this.$t('CONVERSATION.FOOTER.GROUPS_DISABLED_RESTRICTED');
|
|
}
|
|
if (this.isGroupLeft && !this.isOnPrivateNote) {
|
|
return this.$t('CONVERSATION.FOOTER.GROUP_LEFT_RESTRICTED');
|
|
}
|
|
if (this.isAnnouncementModeRestricted && !this.isOnPrivateNote) {
|
|
return this.$t('CONVERSATION.FOOTER.ANNOUNCEMENT_MODE_RESTRICTED');
|
|
}
|
|
if (this.isEditorDisabled) {
|
|
if (this.isAWhatsAppChannel) {
|
|
return this.$t('CONVERSATION.FOOTER.MESSAGING_RESTRICTED_WHATSAPP');
|
|
}
|
|
if (this.isAPIInbox) {
|
|
return this.$t('CONVERSATION.FOOTER.MESSAGING_RESTRICTED_API');
|
|
}
|
|
return this.$t('CONVERSATION.FOOTER.MESSAGING_RESTRICTED');
|
|
}
|
|
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.isEditorDisabled) return true;
|
|
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.isATiktokChannel) {
|
|
return MESSAGE_MAX_LENGTH.TIKTOK;
|
|
}
|
|
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 ||
|
|
this.isATiktokChannel
|
|
);
|
|
},
|
|
replyButtonLabel() {
|
|
let sendMessageText = this.$t('CONVERSATION.REPLYBOX.SEND');
|
|
if (this.isPrivate) {
|
|
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
|
}
|
|
const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
|
|
? `(${this.shortcutKey})`
|
|
: '(↵)';
|
|
return `${sendMessageText} ${keyLabel}`;
|
|
},
|
|
replyBoxClass() {
|
|
return {
|
|
'is-private': this.isPrivate,
|
|
'is-focused': this.isFocused || this.hasAttachments,
|
|
};
|
|
},
|
|
hasAttachments() {
|
|
return this.attachedFiles.length;
|
|
},
|
|
hasRecordedAudio() {
|
|
return this.attachedFiles.some(file => file.isRecordedAudio);
|
|
},
|
|
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;
|
|
},
|
|
messageSignature() {
|
|
return this.getSignatureForInbox(this.inboxId);
|
|
},
|
|
isSignatureAvailable() {
|
|
return !!this.messageSignature;
|
|
},
|
|
sendWithSignature() {
|
|
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
|
},
|
|
conversationId() {
|
|
return this.currentChat.id;
|
|
},
|
|
conversationIdByRoute() {
|
|
return this.conversationId;
|
|
},
|
|
editorStateId() {
|
|
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
|
},
|
|
audioRecordFormat() {
|
|
if (this.isAWhatsAppCloudChannel) {
|
|
return AUDIO_FORMATS.OGG;
|
|
}
|
|
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;
|
|
},
|
|
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
|
|
);
|
|
},
|
|
isDefaultEditorMode() {
|
|
return !this.showAudioRecorderEditor && !this.copilot.isActive.value;
|
|
},
|
|
isEditorDisabled() {
|
|
if (this.isGroupsDisabled && !this.isOnPrivateNote) {
|
|
return true;
|
|
}
|
|
if (this.isGroupLeft && !this.isOnPrivateNote) {
|
|
return true;
|
|
}
|
|
if (this.isAnnouncementModeRestricted && !this.isOnPrivateNote) {
|
|
return true;
|
|
}
|
|
return (
|
|
(this.isAWhatsAppChannel || this.isAPIInbox) &&
|
|
!this.isOnPrivateNote &&
|
|
!this.currentChat.can_reply
|
|
);
|
|
},
|
|
// Signature preview for non-rich editor (WhatsApp, etc.)
|
|
shouldShowSignaturePreview() {
|
|
return (
|
|
this.sendWithSignature &&
|
|
this.messageSignature &&
|
|
!this.isPrivate &&
|
|
!this.showRichContentEditor
|
|
);
|
|
},
|
|
signaturePosition() {
|
|
return this.getSignatureSettingsForInbox(this.inboxId).position;
|
|
},
|
|
signatureSeparator() {
|
|
return this.getSignatureSettingsForInbox(this.inboxId).separator;
|
|
},
|
|
formattedSignature() {
|
|
if (!this.messageSignature) return '';
|
|
return this.formatMessage(this.messageSignature, false, false);
|
|
},
|
|
},
|
|
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();
|
|
// Reset Copilot editor state (includes cancelling ongoing generation)
|
|
this.copilot.reset();
|
|
}
|
|
|
|
if (this.isOnPrivateNote) {
|
|
return;
|
|
}
|
|
|
|
if (canReply || this.isAWhatsAppChannel || this.isAPIInbox) {
|
|
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() {
|
|
// Autosave the current message draft.
|
|
this.doAutoSaveDraft();
|
|
},
|
|
groupContactId: {
|
|
immediate: true,
|
|
handler(contactId) {
|
|
if (
|
|
contactId &&
|
|
this.isAWhatsAppBaileysChannel &&
|
|
this.isGroupConversation &&
|
|
!this.isGroupMembersLoaded
|
|
) {
|
|
this.$store.dispatch('groupMembers/fetch', {
|
|
contactId,
|
|
});
|
|
}
|
|
},
|
|
},
|
|
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 the editor 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);
|
|
emitter.on(CMD_AI_ASSIST, this.executeCopilotAction);
|
|
},
|
|
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
|
|
);
|
|
emitter.off(CMD_AI_ASSIST, this.executeCopilotAction);
|
|
},
|
|
methods: {
|
|
getDraftKey(
|
|
conversationId = this.conversationIdByRoute,
|
|
replyType = this.replyType
|
|
) {
|
|
return `draft-${conversationId}-${replyType}`;
|
|
},
|
|
getCopilotAcceptedMessage(replyType = this.replyType) {
|
|
const key = this.getDraftKey(this.conversationIdByRoute, replyType);
|
|
return this.copilotAcceptedMessages[key] || '';
|
|
},
|
|
setCopilotAcceptedMessage(message, replyType = this.replyType) {
|
|
const key = this.getDraftKey(this.conversationIdByRoute, replyType);
|
|
this.copilotAcceptedMessages[key] = trimContent(message || '');
|
|
},
|
|
clearCopilotAcceptedMessage(replyType = this.replyType) {
|
|
const key = this.getDraftKey(this.conversationIdByRoute, replyType);
|
|
delete this.copilotAcceptedMessages[key];
|
|
},
|
|
handleInsert(article) {
|
|
const { url, title } = article;
|
|
// 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})`
|
|
);
|
|
|
|
useTrack(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
|
|
},
|
|
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 = this.getDraftKey(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 = this.getDraftKey();
|
|
const messageFromStore =
|
|
this.$store.getters['draftMessages/get'](key) || '';
|
|
this.message = messageFromStore;
|
|
}
|
|
},
|
|
removeFromDraft() {
|
|
if (this.conversationIdByRoute) {
|
|
const key = this.getDraftKey();
|
|
this.$store.dispatch('draftMessages/delete', { key });
|
|
}
|
|
},
|
|
getElementToBind() {
|
|
return this.replyEditor;
|
|
},
|
|
getKeyboardEvents() {
|
|
return {
|
|
Escape: {
|
|
action: () => {
|
|
this.hideEmojiPicker();
|
|
},
|
|
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.copilot.isActive.value && this.isFocused) {
|
|
this.onSubmitCopilotReply();
|
|
} else if (this.isAValidEvent('cmd_enter')) {
|
|
this.onSendReply();
|
|
}
|
|
},
|
|
allowOnFocusedInput: true,
|
|
},
|
|
};
|
|
},
|
|
isAValidEvent(selectedKey) {
|
|
return (
|
|
!this.showUserMentions &&
|
|
!this.showMentions &&
|
|
!this.showCannedMenu &&
|
|
!this.showVariablesMenu &&
|
|
!this.showScheduledMessageModal &&
|
|
this.isFocused &&
|
|
this.isEditorHotKeyEnabled(selectedKey)
|
|
);
|
|
},
|
|
applySignatureToMessage(message) {
|
|
if (!this.sendWithSignature || !this.messageSignature) {
|
|
return message;
|
|
}
|
|
const signatureSettings = {
|
|
position: this.signaturePosition,
|
|
separator: this.signatureSeparator,
|
|
};
|
|
return appendSignature(message, this.messageSignature, signatureSettings);
|
|
},
|
|
onPaste(e) {
|
|
// Don't handle paste if compose new conversation modal is open
|
|
if (this.newConversationModalActive) return;
|
|
|
|
// Don't handle paste if editor is disabled
|
|
if (this.isEditorDisabled) return;
|
|
|
|
// NOTE: Don't handle paste if scheduled message modal is open
|
|
if (this.showScheduledMessageModal) return;
|
|
|
|
// Filter valid files (non-zero size)
|
|
Array.from(e.clipboardData.files)
|
|
.filter(file => file.size > 0)
|
|
.filter(file => {
|
|
const isAllowed = isFileTypeAllowedForChannel(file, {
|
|
channelType: this.channelType || this.inbox?.channel_type,
|
|
medium: this.inbox?.medium,
|
|
conversationType: this.conversationType,
|
|
isInstagramChannel: this.isAnInstagramChannel,
|
|
isOnPrivateNote: this.isOnPrivateNote,
|
|
});
|
|
|
|
if (!isAllowed) {
|
|
useAlert(
|
|
this.$t('CONVERSATION.FILE_TYPE_NOT_SUPPORTED', {
|
|
fileName: file.name,
|
|
})
|
|
);
|
|
}
|
|
|
|
return isAllowed;
|
|
})
|
|
.forEach(file => {
|
|
const { name, type, size } = file;
|
|
this.onFileUpload({ name, type, size, 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;
|
|
},
|
|
openScheduledMessageModal() {
|
|
this.showScheduledMessageModal = true;
|
|
},
|
|
closeScheduledMessageModal() {
|
|
this.showScheduledMessageModal = false;
|
|
},
|
|
async onScheduledMessageCreated() {
|
|
this.closeScheduledMessageModal();
|
|
this.clearMessage();
|
|
// NOTE: Open sidebar and expand scheduled messages card
|
|
this.$store.dispatch('updateUISettings', {
|
|
is_contact_sidebar_open: true,
|
|
is_scheduled_messages_open: true,
|
|
});
|
|
},
|
|
confirmOnSendReply() {
|
|
if (this.isReplyButtonDisabled) {
|
|
return;
|
|
}
|
|
if (!this.showMentions) {
|
|
const copilotAcceptedMessage = this.getCopilotAcceptedMessage();
|
|
const isOnWhatsApp = this.isAWhatsAppChannel;
|
|
// Instagram and TikTok do not support sending text and attachments in the same message.
|
|
// For Instagram, combining them causes duplicate messages due to separate echo events per component.
|
|
// For TikTok, the API rejects messages that mix text and media.
|
|
// To handle both cases, text and attachments are always sent as separate messages.
|
|
const isOnInstagram = this.isAnInstagramChannel;
|
|
const isOnTiktok = this.isATiktokChannel;
|
|
if ((isOnWhatsApp || isOnInstagram || isOnTiktok) && !this.isPrivate) {
|
|
this.sendMessageAsMultipleMessages(
|
|
this.message,
|
|
copilotAcceptedMessage
|
|
);
|
|
} else {
|
|
const messagePayload = this.getMessagePayload(this.message);
|
|
this.sendMessage(
|
|
messagePayload,
|
|
this.message,
|
|
copilotAcceptedMessage
|
|
);
|
|
}
|
|
|
|
if (!this.isPrivate) {
|
|
this.clearEmailField();
|
|
}
|
|
|
|
this.clearMessage();
|
|
this.hideEmojiPicker();
|
|
this.$emit('update:popOutReplyBox', false);
|
|
}
|
|
},
|
|
sendMessageAsMultipleMessages(message, copilotAcceptedMessage = '') {
|
|
const messages = this.getMultipleMessagesPayload(message);
|
|
messages.forEach(messagePayload => {
|
|
this.sendMessage(
|
|
messagePayload,
|
|
messagePayload.message || '',
|
|
copilotAcceptedMessage
|
|
);
|
|
});
|
|
},
|
|
sendMessageAnalyticsData(
|
|
isPrivate,
|
|
{ editorMessage = '', copilotAcceptedMessage = '' } = {}
|
|
) {
|
|
const normalizeForComparison = message => trimContent(message || '');
|
|
|
|
const normalizedAcceptedMessage = normalizeForComparison(
|
|
copilotAcceptedMessage
|
|
);
|
|
const normalizedEditorMessage = normalizeForComparison(editorMessage);
|
|
|
|
if (normalizedAcceptedMessage && normalizedEditorMessage) {
|
|
useTrack(CAPTAIN_EVENTS.AI_ASSISTED_MESSAGE_SENT, {
|
|
conversationId: this.conversationIdByRoute,
|
|
channelType: this.channelType,
|
|
editedBeforeSend:
|
|
normalizedAcceptedMessage !== normalizedEditorMessage,
|
|
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,
|
|
editorMessage = '',
|
|
copilotAcceptedMessage = ''
|
|
) {
|
|
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, {
|
|
editorMessage,
|
|
copilotAcceptedMessage,
|
|
});
|
|
} 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) {
|
|
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.isAPIInbox)
|
|
this.replyType = mode;
|
|
if (this.isRecordingAudio) {
|
|
this.toggleAudioRecorder();
|
|
}
|
|
},
|
|
clearEditorSelection() {
|
|
this.updateEditorSelectionWith = '';
|
|
},
|
|
addIntoEditor(content) {
|
|
this.updateEditorSelectionWith = content;
|
|
this.onFocus();
|
|
},
|
|
executeCopilotAction(action, data) {
|
|
this.copilot.execute(action, data);
|
|
},
|
|
clearMessage() {
|
|
this.message = '';
|
|
this.clearCopilotAcceptedMessage();
|
|
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;
|
|
if (!this.isRecordingAudio) {
|
|
this.resetAudioRecorderInput();
|
|
this.onTypingOff();
|
|
} else {
|
|
this.onRecording();
|
|
}
|
|
},
|
|
toggleAudioRecorderPlayPause() {
|
|
if (!this.$refs.audioRecorderInput) return;
|
|
if (!this.recordingAudioState) {
|
|
this.$refs.audioRecorderInput.stopRecording();
|
|
} else {
|
|
this.onTypingOff();
|
|
this.$refs.audioRecorderInput.playPause();
|
|
}
|
|
},
|
|
hideEmojiPicker() {
|
|
if (this.showEmojiPicker) {
|
|
this.toggleEmojiPicker();
|
|
}
|
|
},
|
|
onTypingOn() {
|
|
this.toggleTyping('on');
|
|
},
|
|
onRecording() {
|
|
this.toggleTyping('recording');
|
|
},
|
|
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.removeRecordedAudio();
|
|
|
|
// 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);
|
|
},
|
|
onRecordError() {
|
|
this.toggleAudioRecorder();
|
|
useAlert(this.$t('CONVERSATION.REPLYBOX.AUDIO_CONVERSION_FAILED'));
|
|
},
|
|
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 }) {
|
|
if (file?.isRecordedAudio) {
|
|
this.removeRecordedAudio();
|
|
}
|
|
|
|
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 = [];
|
|
|
|
const messageWithSignature = this.applySignatureToMessage(message);
|
|
|
|
if (this.attachedFiles?.length) {
|
|
let caption =
|
|
this.isAnInstagramChannel || this.isATiktokChannel
|
|
? ''
|
|
: messageWithSignature;
|
|
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,
|
|
};
|
|
|
|
if (attachment.isRecordedAudio) {
|
|
attachmentPayload.isRecordedAudio = this.globalConfig
|
|
.directUploadsEnabled
|
|
? true
|
|
: [attachment.resource.file.name];
|
|
}
|
|
|
|
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
|
|
multipleMessagePayload.push(attachmentPayload);
|
|
// For WhatsApp, only the first attachment gets a caption
|
|
if (!this.isAnInstagramChannel) caption = '';
|
|
});
|
|
}
|
|
|
|
const hasNoAttachments = !this.attachedFiles?.length;
|
|
// For Instagram and TikTok, text must always be sent as a separate message (no captions on attachments).
|
|
// For WhatsApp, we only need a text message if there are no attachments.
|
|
if (
|
|
((this.isAnInstagramChannel || this.isATiktokChannel) &&
|
|
this.message) ||
|
|
(!(this.isAnInstagramChannel || this.isATiktokChannel) &&
|
|
hasNoAttachments)
|
|
) {
|
|
let messagePayload = {
|
|
conversationId: this.currentChat.id,
|
|
message: messageWithSignature,
|
|
private: false,
|
|
sender: this.sender,
|
|
};
|
|
|
|
messagePayload = this.setReplyToInPayload(messagePayload);
|
|
|
|
multipleMessagePayload.push(messagePayload);
|
|
}
|
|
|
|
return multipleMessagePayload;
|
|
},
|
|
getMessagePayload(message) {
|
|
let finalMessage = this.getMessageWithQuotedEmailText(message);
|
|
if (!this.isPrivate) {
|
|
finalMessage = this.applySignatureToMessage(finalMessage);
|
|
}
|
|
|
|
let messagePayload = {
|
|
conversationId: this.currentChat.id,
|
|
message: finalMessage,
|
|
private: this.isPrivate,
|
|
sender: this.sender,
|
|
};
|
|
messagePayload = this.setReplyToInPayload(messagePayload);
|
|
|
|
if (this.attachedFiles?.length) {
|
|
messagePayload.files = [];
|
|
messagePayload.isRecordedAudio = [];
|
|
this.attachedFiles.forEach(attachment => {
|
|
if (this.globalConfig.directUploadsEnabled) {
|
|
messagePayload.files.push(attachment.blobSignedId);
|
|
if (attachment.isRecordedAudio) {
|
|
messagePayload.isRecordedAudio = true;
|
|
}
|
|
} else {
|
|
messagePayload.files.push(attachment.resource.file);
|
|
if (attachment.isRecordedAudio) {
|
|
messagePayload.isRecordedAudio.push(
|
|
attachment.resource.file.name
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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 = '';
|
|
// Only clear the recorded audio when we click toggle button.
|
|
this.removeRecordedAudio();
|
|
},
|
|
removeRecordedAudio() {
|
|
this.attachedFiles = this.attachedFiles.filter(
|
|
file => !file?.isRecordedAudio
|
|
);
|
|
},
|
|
togglePopout() {
|
|
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
|
|
},
|
|
onSubmitCopilotReply() {
|
|
const acceptedMessage = this.copilot.accept();
|
|
this.message = acceptedMessage;
|
|
this.setCopilotAcceptedMessage(acceptedMessage);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<ReplyBoxBanner :message="message" :is-on-private-note="isOnPrivateNote" />
|
|
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
|
|
<ReplyTopPanel
|
|
:mode="replyType"
|
|
:conversation-id="conversationId"
|
|
:is-reply-restricted="isReplyRestricted"
|
|
:disabled="
|
|
(copilot.isActive.value && copilot.isButtonDisabled.value) ||
|
|
showAudioRecorderEditor
|
|
"
|
|
:is-editor-disabled="isEditorDisabled"
|
|
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
|
:characters-remaining="charactersRemaining"
|
|
:editor-content="message"
|
|
:popout-reply-box="popOutReplyBox"
|
|
@set-reply-mode="setReplyMode"
|
|
@toggle-popout="togglePopout"
|
|
@toggle-copilot="copilot.toggleEditor"
|
|
@execute-copilot-action="executeCopilotAction"
|
|
/>
|
|
<ArticleSearchPopover
|
|
v-if="showArticleSearchPopover && connectedPortalSlug"
|
|
:selected-portal-slug="connectedPortalSlug"
|
|
@insert="handleInsert"
|
|
@close="onSearchPopoverClose"
|
|
/>
|
|
<Transition
|
|
mode="out-in"
|
|
enter-active-class="transition-all duration-300 ease-out"
|
|
enter-from-class="opacity-0 translate-y-2 scale-[0.98]"
|
|
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
leave-active-class="transition-all duration-200 ease-in"
|
|
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
leave-to-class="opacity-0 translate-y-2 scale-[0.98]"
|
|
>
|
|
<div :key="copilot.editorTransitionKey.value" class="reply-box__top">
|
|
<ReplyToMessage
|
|
v-if="shouldShowReplyToMessage"
|
|
:message="inReplyTo"
|
|
@dismiss="resetReplyToMessage"
|
|
/>
|
|
<EmojiInput
|
|
v-if="showEmojiPicker"
|
|
v-on-clickaway="hideEmojiPicker"
|
|
:class="{
|
|
'emoji-dialog--expanded': isOnExpandedLayout || popOutReplyBox,
|
|
}"
|
|
:on-click="addIntoEditor"
|
|
/>
|
|
<ReplyEmailHead
|
|
v-if="showReplyHead && isDefaultEditorMode"
|
|
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"
|
|
@record-error="onRecordError"
|
|
@play="recordingAudioState = 'playing'"
|
|
@pause="recordingAudioState = 'paused'"
|
|
/>
|
|
<CopilotEditorSection
|
|
v-if="copilot.isActive.value && !showAudioRecorderEditor"
|
|
:show-copilot-editor="copilot.showEditor.value"
|
|
:is-generating-content="copilot.isGenerating.value"
|
|
:generated-content="copilot.generatedContent.value"
|
|
:is-popout="popOutReplyBox"
|
|
:placeholder="$t('CONVERSATION.FOOTER.COPILOT_MSG_INPUT')"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
@clear-selection="clearEditorSelection"
|
|
@close="copilot.showEditor.value = false"
|
|
@content-ready="copilot.setContentReady"
|
|
@send="copilot.sendFollowUp"
|
|
/>
|
|
<WootMessageEditor
|
|
v-else-if="!showAudioRecorderEditor"
|
|
v-model="message"
|
|
:conversation-id="conversationId"
|
|
:editor-id="editorStateId"
|
|
class="input popover-prosemirror-menu"
|
|
:is-private="isOnPrivateNote"
|
|
:placeholder="messagePlaceHolder"
|
|
:update-selection-with="updateEditorSelectionWith"
|
|
:min-height="4"
|
|
:disabled="isEditorDisabled"
|
|
enable-variables
|
|
:variables="messageVariables"
|
|
:signature="messageSignature"
|
|
allow-signature
|
|
:signature-position-override="signaturePosition"
|
|
:signature-separator-override="signatureSeparator"
|
|
:channel-type="channelType"
|
|
:medium="inbox.medium"
|
|
:is-group-conversation="isGroupConversation"
|
|
:group-contact-id="groupContactId"
|
|
:inbox-phone-number="inboxPhoneNumber"
|
|
@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"
|
|
@execute-copilot-action="executeCopilotAction"
|
|
/>
|
|
|
|
<QuotedEmailPreview
|
|
v-if="shouldShowQuotedPreview && isDefaultEditorMode"
|
|
:quoted-email-text="quotedEmailText"
|
|
:preview-text="quotedEmailPreviewText"
|
|
class="mb-2"
|
|
@toggle="toggleQuotedReply"
|
|
/>
|
|
|
|
<div
|
|
v-if="hasAttachments && isDefaultEditorMode"
|
|
class="bg-transparent py-0 mb-2"
|
|
@paste="onPaste"
|
|
>
|
|
<AttachmentPreview
|
|
class="mt-2"
|
|
:attachments="attachedFiles"
|
|
@remove-attachment="removeAttachment"
|
|
/>
|
|
</div>
|
|
<MessageSignatureMissingAlert
|
|
v-if="
|
|
isSignatureEnabledForInbox &&
|
|
!isSignatureAvailable &&
|
|
isDefaultEditorMode
|
|
"
|
|
class="mb-2"
|
|
/>
|
|
</div>
|
|
</Transition>
|
|
|
|
<Transition
|
|
mode="out-in"
|
|
enter-active-class="transition-all duration-300 ease-out"
|
|
enter-from-class="opacity-0 translate-y-2 scale-[0.98]"
|
|
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
leave-active-class="transition-all duration-200 ease-in"
|
|
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
leave-to-class="opacity-0 translate-y-2 scale-[0.98]"
|
|
>
|
|
<CopilotReplyBottomPanel
|
|
v-if="copilot.isActive.value"
|
|
key="copilot-bottom-panel"
|
|
:is-generating-content="copilot.isButtonDisabled.value"
|
|
@submit="onSubmitCopilotReply"
|
|
@cancel="copilot.reset"
|
|
/>
|
|
<ReplyBottomPanel
|
|
v-else
|
|
key="reply-bottom-panel"
|
|
: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"
|
|
:is-editor-disabled="isEditorDisabled"
|
|
: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-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"
|
|
:show-schedule-options="!isPrivate"
|
|
@select-whatsapp-template="openWhatsappTemplateModal"
|
|
@select-content-template="openContentTemplateModal"
|
|
@replace-text="replaceText"
|
|
@toggle-insert-article="toggleInsertArticle"
|
|
@toggle-quoted-reply="toggleQuotedReply"
|
|
@schedule-message="openScheduledMessageModal"
|
|
/>
|
|
</Transition>
|
|
|
|
<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"
|
|
/>
|
|
|
|
<ScheduledMessageModal
|
|
v-model:show="showScheduledMessageModal"
|
|
:conversation-id="conversationId"
|
|
:inbox-id="inbox.id"
|
|
:initial-content="message"
|
|
:initial-attachment="attachedFiles[0] || null"
|
|
@scheduled-message-created="onScheduledMessageCreated"
|
|
/>
|
|
|
|
<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;
|
|
}
|
|
|
|
.reply-box {
|
|
@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-3 -mt-px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
}
|
|
</style>
|