595 lines
16 KiB
Vue
595 lines
16 KiB
Vue
<template>
|
|
<div class="reply-box" :class="replyBoxClass">
|
|
<reply-top-panel
|
|
:mode="replyType"
|
|
:set-reply-mode="setReplyMode"
|
|
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
|
:characters-remaining="charactersRemaining"
|
|
:popout-reply-box="popoutReplyBox"
|
|
@click="$emit('click')"
|
|
/>
|
|
<div class="reply-box__top">
|
|
<canned-response
|
|
v-if="showMentions && hasSlashCommand"
|
|
v-on-clickaway="hideMentions"
|
|
:search-key="mentionSearchKey"
|
|
@click="replaceText"
|
|
/>
|
|
<emoji-input
|
|
v-if="showEmojiPicker"
|
|
v-on-clickaway="hideEmojiPicker"
|
|
:on-click="emojiOnClick"
|
|
/>
|
|
<reply-email-head
|
|
v-if="showReplyHead"
|
|
:cc-emails.sync="ccEmails"
|
|
:bcc-emails.sync="bccEmails"
|
|
/>
|
|
<resizable-text-area
|
|
v-if="!showRichContentEditor"
|
|
ref="messageInput"
|
|
v-model="message"
|
|
class="input"
|
|
:placeholder="messagePlaceHolder"
|
|
:min-height="4"
|
|
@typing-off="onTypingOff"
|
|
@typing-on="onTypingOn"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
/>
|
|
<woot-message-editor
|
|
v-else
|
|
v-model="message"
|
|
class="input"
|
|
:is-private="isOnPrivateNote"
|
|
:placeholder="messagePlaceHolder"
|
|
:min-height="4"
|
|
@typing-off="onTypingOff"
|
|
@typing-on="onTypingOn"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
@toggle-user-mention="toggleUserMention"
|
|
@toggle-canned-menu="toggleCannedMenu"
|
|
/>
|
|
</div>
|
|
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
|
<attachment-preview
|
|
:attachments="attachedFiles"
|
|
:remove-attachment="removeAttachment"
|
|
/>
|
|
</div>
|
|
<reply-bottom-panel
|
|
:mode="replyType"
|
|
:send-button-text="replyButtonLabel"
|
|
:on-direct-file-upload="onDirectFileUpload"
|
|
:show-file-upload="showFileUpload"
|
|
:toggle-emoji-picker="toggleEmojiPicker"
|
|
:show-emoji-picker="showEmojiPicker"
|
|
:on-send="sendMessage"
|
|
:is-send-disabled="isReplyButtonDisabled"
|
|
:set-format-mode="setFormatMode"
|
|
:is-on-private-note="isOnPrivateNote"
|
|
:is-format-mode="showRichContentEditor"
|
|
:enable-rich-editor="isRichEditorEnabled"
|
|
:enter-to-send-enabled="enterToSendEnabled"
|
|
:enable-multiple-file-upload="enableMultipleFileUpload"
|
|
@toggleEnterToSend="toggleEnterToSend"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import { mixin as clickaway } from 'vue-clickaway';
|
|
import alertMixin from 'shared/mixins/alertMixin';
|
|
|
|
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
|
import CannedResponse from './CannedResponse';
|
|
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
|
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
|
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
|
|
import ReplyEmailHead from './ReplyEmailHead';
|
|
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
|
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
|
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
|
|
|
import {
|
|
isEscape,
|
|
isEnter,
|
|
hasPressedShift,
|
|
hasPressedCommandPlusKKey,
|
|
} 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 { DirectUpload } from 'activestorage';
|
|
|
|
export default {
|
|
components: {
|
|
EmojiInput,
|
|
CannedResponse,
|
|
ResizableTextArea,
|
|
AttachmentPreview,
|
|
ReplyTopPanel,
|
|
ReplyEmailHead,
|
|
ReplyBottomPanel,
|
|
WootMessageEditor,
|
|
},
|
|
mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
|
|
props: {
|
|
selectedTweet: {
|
|
type: [Object, String],
|
|
default: () => ({}),
|
|
},
|
|
isATweet: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
popoutReplyBox: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
message: '',
|
|
isFocused: false,
|
|
showEmojiPicker: false,
|
|
showMentions: false,
|
|
attachedFiles: [],
|
|
isUploading: false,
|
|
replyType: REPLY_EDITOR_MODES.REPLY,
|
|
mentionSearchKey: '',
|
|
hasUserMention: false,
|
|
hasSlashCommand: false,
|
|
bccEmails: '',
|
|
ccEmails: '',
|
|
};
|
|
},
|
|
computed: {
|
|
showRichContentEditor() {
|
|
if (this.isOnPrivateNote) {
|
|
return true;
|
|
}
|
|
|
|
if (this.isRichEditorEnabled) {
|
|
const {
|
|
display_rich_content_editor: displayRichContentEditor,
|
|
} = this.uiSettings;
|
|
|
|
return displayRichContentEditor;
|
|
}
|
|
return false;
|
|
},
|
|
...mapGetters({ currentChat: 'getSelectedChat' }),
|
|
enterToSendEnabled() {
|
|
return !!this.uiSettings.enter_to_send_enabled;
|
|
},
|
|
isPrivate() {
|
|
if (this.currentChat.can_reply || this.isAWhatsappChannel) {
|
|
return this.isOnPrivateNote;
|
|
}
|
|
return true;
|
|
},
|
|
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.isATweet && !this.inReplyTo && !this.isOnPrivateNote) {
|
|
return true;
|
|
}
|
|
|
|
if (this.hasAttachments) return false;
|
|
|
|
return (
|
|
this.isMessageEmpty ||
|
|
this.message.length === 0 ||
|
|
this.message.length > this.maxLength
|
|
);
|
|
},
|
|
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.isAWhatsappChannel) {
|
|
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
|
}
|
|
if (this.isATwilioSMSChannel) {
|
|
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
|
}
|
|
if (this.isATwitterInbox) {
|
|
if (this.conversationType === 'tweet') {
|
|
return MESSAGE_MAX_LENGTH.TWEET - this.replyToUserLength - 2;
|
|
}
|
|
}
|
|
return MESSAGE_MAX_LENGTH.GENERAL;
|
|
},
|
|
showFileUpload() {
|
|
return (
|
|
this.isAWebWidgetInbox ||
|
|
this.isAFacebookInbox ||
|
|
this.isAWhatsappChannel ||
|
|
this.isAPIInbox ||
|
|
this.isAnEmailChannel ||
|
|
this.isATwilioSMSChannel ||
|
|
this.isATelegramChannel
|
|
);
|
|
},
|
|
replyButtonLabel() {
|
|
if (this.isPrivate) {
|
|
return this.$t('CONVERSATION.REPLYBOX.CREATE');
|
|
}
|
|
if (this.conversationType === 'tweet') {
|
|
return this.$t('CONVERSATION.REPLYBOX.TWEET');
|
|
}
|
|
return this.$t('CONVERSATION.REPLYBOX.SEND');
|
|
},
|
|
replyBoxClass() {
|
|
return {
|
|
'is-private': this.isPrivate,
|
|
'is-focused': this.isFocused || this.hasAttachments,
|
|
};
|
|
},
|
|
hasAttachments() {
|
|
return this.attachedFiles.length;
|
|
},
|
|
isRichEditorEnabled() {
|
|
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
|
},
|
|
isOnPrivateNote() {
|
|
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
|
},
|
|
inReplyTo() {
|
|
const selectedTweet = this.selectedTweet || {};
|
|
return selectedTweet.id;
|
|
},
|
|
replyToUserLength() {
|
|
const selectedTweet = this.selectedTweet || {};
|
|
const {
|
|
sender: {
|
|
additional_attributes: { screen_name: screenName = '' } = {},
|
|
} = {},
|
|
} = selectedTweet;
|
|
return screenName ? screenName.length : 0;
|
|
},
|
|
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;
|
|
},
|
|
},
|
|
watch: {
|
|
currentChat(conversation) {
|
|
const { can_reply: canReply } = conversation;
|
|
if (this.isOnPrivateNote) {
|
|
return;
|
|
}
|
|
|
|
if (canReply || this.isAWhatsappChannel) {
|
|
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
|
} else {
|
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
|
}
|
|
},
|
|
message(updatedMessage) {
|
|
this.hasSlashCommand =
|
|
updatedMessage[0] === '/' && !this.showRichContentEditor;
|
|
const hasNextWord = updatedMessage.includes(' ');
|
|
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
|
if (isShortCodeActive) {
|
|
this.mentionSearchKey = updatedMessage.substr(1, updatedMessage.length);
|
|
this.showMentions = true;
|
|
} else {
|
|
this.mentionSearchKey = '';
|
|
this.showMentions = false;
|
|
}
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
// Donot use the keyboard listener mixin here as the events here are supposed to be
|
|
// working even if input/textarea is focussed.
|
|
document.addEventListener('keydown', this.handleKeyEvents);
|
|
document.addEventListener('paste', this.onPaste);
|
|
},
|
|
destroyed() {
|
|
document.removeEventListener('keydown', this.handleKeyEvents);
|
|
document.removeEventListener('paste', this.onPaste);
|
|
},
|
|
methods: {
|
|
onPaste(e) {
|
|
const data = e.clipboardData.files;
|
|
if (!data.length || !data[0]) {
|
|
return;
|
|
}
|
|
const file = data[0];
|
|
const { name, type, size } = file;
|
|
this.onFileUpload({ name, type, size, file });
|
|
},
|
|
toggleUserMention(currentMentionState) {
|
|
this.hasUserMention = currentMentionState;
|
|
},
|
|
toggleCannedMenu(value) {
|
|
this.showCannedMenu = value;
|
|
},
|
|
handleKeyEvents(e) {
|
|
if (isEscape(e)) {
|
|
this.hideEmojiPicker();
|
|
this.hideMentions();
|
|
} else if (isEnter(e)) {
|
|
const hasSendOnEnterEnabled =
|
|
(this.showRichContentEditor &&
|
|
this.enterToSendEnabled &&
|
|
!this.hasUserMention &&
|
|
!this.showCannedMenu) ||
|
|
!this.showRichContentEditor;
|
|
const shouldSendMessage =
|
|
hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused;
|
|
if (shouldSendMessage) {
|
|
e.preventDefault();
|
|
this.sendMessage();
|
|
}
|
|
} else if (hasPressedCommandPlusKKey(e)) {
|
|
this.openCommandBar();
|
|
}
|
|
},
|
|
openCommandBar() {
|
|
const ninja = document.querySelector('ninja-keys');
|
|
ninja.open();
|
|
},
|
|
toggleEnterToSend(enterToSendEnabled) {
|
|
this.updateUISettings({ enter_to_send_enabled: enterToSendEnabled });
|
|
},
|
|
async sendMessage() {
|
|
if (this.isReplyButtonDisabled) {
|
|
return;
|
|
}
|
|
if (!this.showMentions) {
|
|
const newMessage = this.message;
|
|
const messagePayload = this.getMessagePayload(newMessage);
|
|
this.clearMessage();
|
|
try {
|
|
await this.$store.dispatch(
|
|
'createPendingMessageAndSend',
|
|
messagePayload
|
|
);
|
|
this.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error?.response?.data?.error ||
|
|
this.$t('CONVERSATION.MESSAGE_ERROR');
|
|
this.showAlert(errorMessage);
|
|
}
|
|
this.hideEmojiPicker();
|
|
this.$emit('update:popoutReplyBox', false);
|
|
}
|
|
},
|
|
replaceText(message) {
|
|
setTimeout(() => {
|
|
this.message = message;
|
|
}, 100);
|
|
},
|
|
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
|
const { can_reply: canReply } = this.currentChat;
|
|
|
|
if (canReply || this.isAWhatsappChannel) this.replyType = mode;
|
|
if (this.showRichContentEditor) {
|
|
return;
|
|
}
|
|
this.$nextTick(() => this.$refs.messageInput.focus());
|
|
},
|
|
emojiOnClick(emoji) {
|
|
this.message = `${this.message}${emoji} `;
|
|
},
|
|
clearMessage() {
|
|
this.message = '';
|
|
this.attachedFiles = [];
|
|
this.ccEmails = '';
|
|
this.bccEmails = '';
|
|
},
|
|
toggleEmojiPicker() {
|
|
this.showEmojiPicker = !this.showEmojiPicker;
|
|
},
|
|
hideEmojiPicker() {
|
|
if (this.showEmojiPicker) {
|
|
this.toggleEmojiPicker();
|
|
}
|
|
},
|
|
hideMentions() {
|
|
this.showMentions = false;
|
|
},
|
|
onTypingOn() {
|
|
this.toggleTyping('on');
|
|
},
|
|
onTypingOff() {
|
|
this.toggleTyping('off');
|
|
},
|
|
onBlur() {
|
|
this.isFocused = false;
|
|
},
|
|
onFocus() {
|
|
this.isFocused = true;
|
|
},
|
|
toggleTyping(status) {
|
|
const conversationId = this.currentChat.id;
|
|
const isPrivate = this.isPrivate;
|
|
this.$store.dispatch('conversationTypingStatus/toggleTyping', {
|
|
status,
|
|
conversationId,
|
|
isPrivate,
|
|
});
|
|
},
|
|
onDirectFileUpload(file) {
|
|
if (!file) {
|
|
return;
|
|
}
|
|
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
|
const upload = new DirectUpload(
|
|
file.file,
|
|
'/rails/active_storage/direct_uploads',
|
|
null,
|
|
file.file.name
|
|
);
|
|
upload.create((error, blob) => {
|
|
if (error) {
|
|
this.showAlert(error);
|
|
} else {
|
|
this.attachedFiles.push({
|
|
currentChatId: this.currentChat.id,
|
|
resource: blob,
|
|
isPrivate: this.isPrivate,
|
|
thumb: null,
|
|
blobSignedId: blob.signed_id,
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
this.showAlert(
|
|
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
|
MAXIMUM_FILE_UPLOAD_SIZE,
|
|
})
|
|
);
|
|
}
|
|
},
|
|
onFileUpload(file) {
|
|
if (!file) {
|
|
return;
|
|
}
|
|
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
|
this.attachedFiles = [];
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file.file);
|
|
reader.onloadend = () => {
|
|
this.attachedFiles.push({
|
|
currentChatId: this.currentChat.id,
|
|
resource: file,
|
|
isPrivate: this.isPrivate,
|
|
thumb: reader.result,
|
|
});
|
|
};
|
|
} else {
|
|
this.showAlert(
|
|
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
|
MAXIMUM_FILE_UPLOAD_SIZE,
|
|
})
|
|
);
|
|
}
|
|
},
|
|
removeAttachment(itemIndex) {
|
|
this.attachedFiles = this.attachedFiles.filter(
|
|
(item, index) => itemIndex !== index
|
|
);
|
|
},
|
|
getMessagePayload(message) {
|
|
const messagePayload = {
|
|
conversationId: this.currentChat.id,
|
|
message,
|
|
private: this.isPrivate,
|
|
};
|
|
|
|
if (this.inReplyTo) {
|
|
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
|
|
}
|
|
|
|
if (this.attachedFiles && this.attachedFiles.length) {
|
|
messagePayload.files = [];
|
|
this.attachedFiles.forEach(attachment => {
|
|
messagePayload.files.push(attachment.blobSignedId);
|
|
});
|
|
}
|
|
|
|
if (this.ccEmails) {
|
|
messagePayload.ccEmails = this.ccEmails;
|
|
}
|
|
|
|
if (this.bccEmails) {
|
|
messagePayload.bccEmails = this.bccEmails;
|
|
}
|
|
|
|
return messagePayload;
|
|
},
|
|
setFormatMode(value) {
|
|
this.updateUISettings({ display_rich_content_editor: value });
|
|
},
|
|
setCcEmails(value) {
|
|
this.bccEmails = value.bccEmails;
|
|
this.ccEmails = value.ccEmails;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.send-button {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.attachment-preview-box {
|
|
padding: 0 var(--space-normal);
|
|
background: transparent;
|
|
}
|
|
|
|
.reply-box {
|
|
border-top: 1px solid var(--color-border);
|
|
background: white;
|
|
|
|
&.is-private {
|
|
background: var(--y-50);
|
|
}
|
|
}
|
|
.send-button {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.reply-box__top {
|
|
padding: 0 var(--space-normal);
|
|
border-top: 1px solid var(--color-border);
|
|
margin-top: -1px;
|
|
}
|
|
|
|
.emoji-dialog {
|
|
top: unset;
|
|
bottom: 12px;
|
|
left: -320px;
|
|
right: unset;
|
|
|
|
&::before {
|
|
right: -16px;
|
|
bottom: 10px;
|
|
transform: rotate(270deg);
|
|
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
|
}
|
|
}
|
|
</style>
|