Compare commits

...
6 changed files with 235 additions and 201 deletions
@@ -2,10 +2,6 @@
transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
}
.reply-box__top {
.icon {
color: $medium-gray;
@@ -47,8 +43,6 @@
}
&.is-private {
@apply bg-yellow-100 dark:bg-yellow-800;
.reply-box__top {
@apply bg-yellow-100 dark:bg-yellow-800;
@@ -1,5 +1,11 @@
<template>
<div class="reply-box" :class="replyBoxClass">
<div
class="border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900 reply-box"
:class="{
'bg-yellow-100 dark:bg-yellow-800 is-private': isPrivate,
'shadow-sm': isFocused || hasAttachments,
}"
>
<banner
v-if="showSelfAssignBanner"
action-button-variant="clear"
@@ -136,48 +142,58 @@
</template>
<script>
// Vuex
import { mapGetters } from 'vuex';
// Mixins
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import CannedResponse from './CannedResponse.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 MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
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 { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import rtlMixin from 'shared/mixins/rtlMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
import replyDraftMixin from 'dashboard/mixins/replyDraftMixin';
import audioRecordingMixin from 'dashboard/mixins/audioRecordingMixin';
import emailEditorMixin from 'dashboard/mixins/emailEditorMixin';
import attachmentHandlerMixin from 'dashboard/mixins/attachmentHandlerMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
// constants
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import wootConstants from 'dashboard/constants/globals';
// components
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
import CannedResponse from './CannedResponse.vue';
import EmojiInput from 'shared/components/emoji/EmojiInput.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
// Helpers
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import {
appendSignature,
removeSignature,
replaceSignature,
extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper';
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
import {
getMessageVariables,
getUndefinedVariablesInMessage,
replaceVariablesInMessage,
debounce,
} from '@chatwoot/utils';
export default {
components: {
@@ -202,6 +218,10 @@ export default {
messageFormatterMixin,
rtlMixin,
fileUploadMixin,
replyDraftMixin,
audioRecordingMixin,
emailEditorMixin,
attachmentHandlerMixin,
],
props: {
selectedTweet: {
@@ -222,17 +242,10 @@ export default {
message: '',
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,
updateEditorSelectionWith: '',
@@ -389,15 +402,6 @@ export default {
: '(↵)';
return `${sendMessageText} ${keyLabel}`;
},
replyBoxClass() {
return {
'is-private': this.isPrivate,
'is-focused': this.isFocused || this.hasAttachments,
};
},
hasAttachments() {
return this.attachedFiles.length;
},
hasRecordedAudio() {
return (
this.$refs.audioRecorderInput &&
@@ -407,12 +411,6 @@ export default {
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;
},
@@ -492,12 +490,6 @@ export default {
editorStateId() {
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
},
audioRecordFormat() {
if (this.isAWhatsAppChannel || this.isAPIInbox) {
return AUDIO_FORMATS.OGG;
}
return AUDIO_FORMATS.WAV;
},
messageVariables() {
const variables = getMessageVariables({
conversation: this.currentChat,
@@ -595,31 +587,6 @@ export default {
);
}
},
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;
@@ -629,12 +596,6 @@ export default {
? 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 });
}
},
handleKeyEvents(e) {
const keyCode = buildHotKeys(e);
if (keyCode === 'escape') {
@@ -856,32 +817,9 @@ export default {
this.attachedFiles = [];
this.isRecordingAudio = false;
},
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.clearMessage();
this.clearEmailField();
}
},
toggleAudioRecorderPlayPause() {
if (this.isRecordingAudio) {
if (!this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopAudioRecording();
} else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
}
}
},
hideEmojiPicker() {
if (this.showEmojiPicker) {
this.toggleEmojiPicker();
@@ -903,18 +841,6 @@ export default {
onFocus() {
this.isFocused = true;
},
onStateProgressRecorderChanged(duration) {
this.recordingAudioDurationText = duration;
},
onStateRecorderChanged(state) {
this.recordingAudioState = state;
if (state && 'notallowederror'.includes(state)) {
this.toggleAudioRecorder();
}
},
onFinishRecorder(file) {
return file && this.onFileUpload(file);
},
toggleTyping(status) {
const conversationId = this.currentChat.id;
const isPrivate = this.isPrivate;
@@ -929,24 +855,6 @@ export default {
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,
});
};
},
removeAttachment(itemIndex) {
this.attachedFiles = this.attachedFiles.filter(
(item, index) => itemIndex !== index
);
},
getMessagePayloadForWhatsapp(message) {
const multipleMessagePayload = [];
@@ -1015,51 +923,6 @@ export default {
return messagePayload;
},
setCcEmails(value) {
this.bccEmails = value.bccEmails;
this.ccEmails = value.ccEmails;
},
setCCAndToEmailsFromLastChat() {
if (!this.lastEmail) return;
const {
content_attributes: { email: emailAttributes = {} },
} = this.lastEmail;
// Retrieve the email of the current conversation's sender
const conversationContact = this.currentChat?.meta?.sender?.email || '';
let cc = emailAttributes.cc ? [...emailAttributes.cc] : [];
let to = [];
// there might be a situation where the current conversation will include a message from a third person,
// and the current conversation contact is in CC.
// This is an edge-case, reported here: CW-1511 [ONLY FOR INTERNAL REFERENCE]
// So we remove the current conversation contact's email from the CC list if present
if (cc.includes(conversationContact)) {
cc = cc.filter(email => email !== conversationContact);
}
// If the last incoming message sender is different from the conversation contact, add them to the "to"
// and add the conversation contact to the CC
if (!emailAttributes.from.includes(conversationContact)) {
to.push(...emailAttributes.from);
cc.push(conversationContact);
}
// Remove the conversation contact's email from the BCC list if present
let bcc = (emailAttributes.bcc || []).filter(
email => email !== conversationContact
);
// Ensure only unique email addresses are in the CC list
bcc = [...new Set(bcc)];
cc = [...new Set(cc)];
to = [...new Set(to)];
this.ccEmails = cc.join(', ');
this.bccEmails = bcc.join(', ');
this.toEmails = to.join(', ');
},
},
};
</script>
@@ -1077,13 +940,6 @@ export default {
@apply bg-transparent py-0 px-4;
}
.reply-box {
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
&.is-private {
@apply bg-yellow-50 dark:bg-yellow-200;
}
}
.send-button {
@apply mb-0;
}
@@ -0,0 +1,35 @@
export default {
data() {
return {
attachedFiles: [],
};
},
computed: {
hasAttachments() {
return this.attachedFiles.length;
},
},
methods: {
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,
});
};
},
removeAttachment(itemIndex) {
this.attachedFiles = this.attachedFiles.filter(
(item, index) => itemIndex !== index
);
},
clearAttachments() {
this.attachedFiles = [];
},
},
};
@@ -0,0 +1,58 @@
import { AUDIO_FORMATS } from 'shared/constants/messages';
export default {
data() {
return {
isRecordingAudio: false,
isRecorderAudioStopped: false,
recordingAudioState: '',
recordingAudioDurationText: '',
};
},
computed: {
audioRecordFormat() {
if (this.isAWhatsAppChannel || this.isAPIInbox) {
return AUDIO_FORMATS.OGG;
}
return AUDIO_FORMATS.WAV;
},
showAudioRecorder() {
return !this.isOnPrivateNote && this.showFileUpload;
},
showAudioRecorderEditor() {
return this.showAudioRecorder && this.isRecordingAudio;
},
},
methods: {
toggleAudioRecorder() {
this.isRecordingAudio = !this.isRecordingAudio;
this.isRecorderAudioStopped = !this.isRecordingAudio;
if (!this.isRecordingAudio) {
this.clearMessage();
this.clearEmailField();
}
},
toggleAudioRecorderPlayPause() {
if (this.isRecordingAudio) {
if (!this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopAudioRecording();
} else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
}
}
},
onStateProgressRecorderChanged(duration) {
this.recordingAudioDurationText = duration;
},
onStateRecorderChanged(state) {
this.recordingAudioState = state;
if (state && 'notallowederror'.includes(state)) {
this.toggleAudioRecorder();
}
},
onFinishRecorder(file) {
return file && this.onFileUpload(file);
},
},
};
@@ -0,0 +1,54 @@
// emailHandlingMixin.js
export default {
data() {
return {
ccEmails: '',
bccEmails: '',
toEmails: '',
};
},
methods: {
setCcEmails(value) {
this.bccEmails = value.bccEmails;
this.ccEmails = value.ccEmails;
},
setCCAndToEmailsFromLastChat() {
if (!this.lastEmail) return;
const {
content_attributes: { email: emailAttributes = {} },
} = this.lastEmail;
const conversationContact = this.currentChat?.meta?.sender?.email || '';
let cc = emailAttributes.cc ? [...emailAttributes.cc] : [];
let to = [];
if (cc.includes(conversationContact)) {
cc = cc.filter(email => email !== conversationContact);
}
if (!emailAttributes.from.includes(conversationContact)) {
to.push(...emailAttributes.from);
cc.push(conversationContact);
}
let bcc = (emailAttributes.bcc || []).filter(
email => email !== conversationContact
);
bcc = [...new Set(bcc)];
cc = [...new Set(cc)];
to = [...new Set(to)];
this.ccEmails = cc.join(', ');
this.bccEmails = bcc.join(', ');
this.toEmails = to.join(', ');
},
clearEmailField() {
this.ccEmails = '';
this.bccEmails = '';
this.toEmails = '';
},
},
};
@@ -0,0 +1,37 @@
import { trimContent } from '@chatwoot/utils';
export default {
methods: {
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);
}
},
removeFromDraft() {
if (this.conversationIdByRoute) {
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
this.$store.dispatch('draftMessages/delete', { key });
}
},
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 = '';
},
},
};