feat: Add support for draft messages in reply box (#4440)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
parent
beedfc47bf
commit
8df7547043
3 changed files with 100 additions and 1 deletions
|
@ -147,6 +147,7 @@ import {
|
||||||
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
|
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
|
||||||
} from 'shared/constants/messages';
|
} from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||||
|
@ -154,6 +155,8 @@ import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import { DirectUpload } from 'activestorage';
|
import { DirectUpload } from 'activestorage';
|
||||||
import { frontendURL } from '../../../helper/URLHelper';
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
|
import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage';
|
||||||
|
import { trimContent, debounce } from '@chatwoot/utils';
|
||||||
import wootConstants from 'dashboard/constants';
|
import wootConstants from 'dashboard/constants';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
|
@ -209,6 +212,7 @@ export default {
|
||||||
hasSlashCommand: false,
|
hasSlashCommand: false,
|
||||||
bccEmails: '',
|
bccEmails: '',
|
||||||
ccEmails: '',
|
ccEmails: '',
|
||||||
|
doAutoSaveDraft: () => {},
|
||||||
showWhatsAppTemplatesModal: false,
|
showWhatsAppTemplatesModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -434,6 +438,13 @@ export default {
|
||||||
enterToSendEnabled() {
|
enterToSendEnabled() {
|
||||||
return this.editorMessageKey === 'enter';
|
return this.editorMessageKey === 'enter';
|
||||||
},
|
},
|
||||||
|
conversationId() {
|
||||||
|
return this.currentChat.id;
|
||||||
|
},
|
||||||
|
conversationIdByRoute() {
|
||||||
|
const { conversation_id: conversationId } = this.$route.params;
|
||||||
|
return conversationId;
|
||||||
|
},
|
||||||
editorStateId() {
|
editorStateId() {
|
||||||
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
},
|
},
|
||||||
|
@ -441,6 +452,7 @@ export default {
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation) {
|
currentChat(conversation) {
|
||||||
const { can_reply: canReply } = conversation;
|
const { can_reply: canReply } = conversation;
|
||||||
|
|
||||||
if (this.isOnPrivateNote) {
|
if (this.isOnPrivateNote) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -453,6 +465,12 @@ export default {
|
||||||
|
|
||||||
this.setCCEmailFromLastChat();
|
this.setCCEmailFromLastChat();
|
||||||
},
|
},
|
||||||
|
conversationIdByRoute(conversationId, oldConversationId) {
|
||||||
|
if (conversationId !== oldConversationId) {
|
||||||
|
this.setToDraft(oldConversationId, this.replyType);
|
||||||
|
this.getFromDraft();
|
||||||
|
}
|
||||||
|
},
|
||||||
message(updatedMessage) {
|
message(updatedMessage) {
|
||||||
this.hasSlashCommand =
|
this.hasSlashCommand =
|
||||||
updatedMessage[0] === '/' && !this.showRichContentEditor;
|
updatedMessage[0] === '/' && !this.showRichContentEditor;
|
||||||
|
@ -465,21 +483,86 @@ export default {
|
||||||
this.mentionSearchKey = '';
|
this.mentionSearchKey = '';
|
||||||
this.showMentions = false;
|
this.showMentions = false;
|
||||||
}
|
}
|
||||||
|
this.doAutoSaveDraft();
|
||||||
|
},
|
||||||
|
replyType(updatedReplyType, oldReplyType) {
|
||||||
|
this.setToDraft(this.conversationIdByRoute, oldReplyType);
|
||||||
|
this.getFromDraft();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.getFromDraft();
|
||||||
// Donot use the keyboard listener mixin here as the events here are supposed to be
|
// Donot use the keyboard listener mixin here as the events here are supposed to be
|
||||||
// working even if input/textarea is focussed.
|
// working even if input/textarea is focussed.
|
||||||
document.addEventListener('paste', this.onPaste);
|
document.addEventListener('paste', this.onPaste);
|
||||||
document.addEventListener('keydown', this.handleKeyEvents);
|
document.addEventListener('keydown', this.handleKeyEvents);
|
||||||
this.setCCEmailFromLastChat();
|
this.setCCEmailFromLastChat();
|
||||||
|
this.doAutoSaveDraft = debounce(
|
||||||
|
() => {
|
||||||
|
this.saveDraft(this.conversationIdByRoute, this.replyType);
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
true
|
||||||
|
);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
document.removeEventListener('paste', this.onPaste);
|
document.removeEventListener('paste', this.onPaste);
|
||||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getSavedDraftMessages() {
|
||||||
|
return LocalStorage.get(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES) || {};
|
||||||
|
},
|
||||||
|
saveDraft(conversationId, replyType) {
|
||||||
|
if (this.message || this.message === '') {
|
||||||
|
const savedDraftMessages = this.getSavedDraftMessages();
|
||||||
|
const key = `draft-${conversationId}-${replyType}`;
|
||||||
|
const draftToSave = trimContent(this.message || '');
|
||||||
|
const {
|
||||||
|
[key]: currentDraft,
|
||||||
|
...restOfDraftMessages
|
||||||
|
} = savedDraftMessages;
|
||||||
|
|
||||||
|
const updatedDraftMessages = draftToSave
|
||||||
|
? {
|
||||||
|
...restOfDraftMessages,
|
||||||
|
[key]: draftToSave,
|
||||||
|
}
|
||||||
|
: restOfDraftMessages;
|
||||||
|
|
||||||
|
LocalStorage.set(
|
||||||
|
LOCAL_STORAGE_KEYS.DRAFT_MESSAGES,
|
||||||
|
updatedDraftMessages
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setToDraft(conversationId, replyType) {
|
||||||
|
this.saveDraft(conversationId, replyType);
|
||||||
|
this.message = '';
|
||||||
|
},
|
||||||
|
getFromDraft() {
|
||||||
|
if (this.conversationIdByRoute) {
|
||||||
|
try {
|
||||||
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
|
const savedDraftMessages = this.getSavedDraftMessages();
|
||||||
|
this.message = `${savedDraftMessages[key] || ''}`;
|
||||||
|
} catch (error) {
|
||||||
|
this.message = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeFromDraft() {
|
||||||
|
if (this.conversationIdByRoute) {
|
||||||
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
|
const draftMessages = this.getSavedDraftMessages();
|
||||||
|
const { [key]: toBeRemoved, ...updatedDraftMessages } = draftMessages;
|
||||||
|
LocalStorage.set(
|
||||||
|
LOCAL_STORAGE_KEYS.DRAFT_MESSAGES,
|
||||||
|
updatedDraftMessages
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
handleKeyEvents(e) {
|
handleKeyEvents(e) {
|
||||||
const keyCode = buildHotKeys(e);
|
const keyCode = buildHotKeys(e);
|
||||||
if (keyCode === 'escape') {
|
if (keyCode === 'escape') {
|
||||||
|
@ -564,11 +647,13 @@ export default {
|
||||||
newMessage += '\n\n' + this.messageSignature;
|
newMessage += '\n\n' + this.messageSignature;
|
||||||
}
|
}
|
||||||
const messagePayload = this.getMessagePayload(newMessage);
|
const messagePayload = this.getMessagePayload(newMessage);
|
||||||
|
|
||||||
this.clearMessage();
|
this.clearMessage();
|
||||||
if (!this.isPrivate) {
|
if (!this.isPrivate) {
|
||||||
this.clearEmailField();
|
this.clearEmailField();
|
||||||
}
|
}
|
||||||
this.sendMessage(messagePayload);
|
this.sendMessage(messagePayload);
|
||||||
|
this.clearMessage();
|
||||||
this.hideEmojiPicker();
|
this.hideEmojiPicker();
|
||||||
this.$emit('update:popoutReplyBox', false);
|
this.$emit('update:popoutReplyBox', false);
|
||||||
}
|
}
|
||||||
|
@ -580,6 +665,7 @@ export default {
|
||||||
messagePayload
|
messagePayload
|
||||||
);
|
);
|
||||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||||
|
this.removeFromDraft();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
||||||
|
@ -658,6 +744,7 @@ export default {
|
||||||
},
|
},
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.isFocused = false;
|
this.isFocused = false;
|
||||||
|
this.saveDraft(this.conversationIdByRoute, this.replyType);
|
||||||
},
|
},
|
||||||
onFocus() {
|
onFocus() {
|
||||||
this.isFocused = true;
|
this.isFocused = true;
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import types from '../mutation-types';
|
import types from '../mutation-types';
|
||||||
import authAPI from '../../api/auth';
|
import authAPI from '../../api/auth';
|
||||||
import { setUser, clearCookiesOnLogout } from '../utils/api';
|
|
||||||
|
import {
|
||||||
|
setUser,
|
||||||
|
clearCookiesOnLogout,
|
||||||
|
clearLocalStorageOnLogout,
|
||||||
|
} from '../utils/api';
|
||||||
import { getLoginRedirectURL } from '../../helper/URLHelper';
|
import { getLoginRedirectURL } from '../../helper/URLHelper';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
@ -89,6 +94,7 @@ export const actions = {
|
||||||
authAPI
|
authAPI
|
||||||
.login(credentials)
|
.login(credentials)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
clearLocalStorageOnLogout();
|
||||||
window.location = getLoginRedirectURL({
|
window.location = getLoginRedirectURL({
|
||||||
ssoAccountId,
|
ssoAccountId,
|
||||||
ssoConversationId,
|
ssoConversationId,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
CHATWOOT_RESET,
|
CHATWOOT_RESET,
|
||||||
CHATWOOT_SET_USER,
|
CHATWOOT_SET_USER,
|
||||||
} from '../../helper/scriptHelpers';
|
} from '../../helper/scriptHelpers';
|
||||||
|
import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../helper/localStorage';
|
||||||
|
|
||||||
Cookies.defaults = { sameSite: 'Lax' };
|
Cookies.defaults = { sameSite: 'Lax' };
|
||||||
|
|
||||||
|
@ -37,10 +38,15 @@ export const clearBrowserSessionCookies = () => {
|
||||||
Cookies.remove('user');
|
Cookies.remove('user');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const clearLocalStorageOnLogout = () => {
|
||||||
|
LocalStorage.remove(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES);
|
||||||
|
};
|
||||||
|
|
||||||
export const clearCookiesOnLogout = () => {
|
export const clearCookiesOnLogout = () => {
|
||||||
window.bus.$emit(CHATWOOT_RESET);
|
window.bus.$emit(CHATWOOT_RESET);
|
||||||
window.bus.$emit(ANALYTICS_RESET);
|
window.bus.$emit(ANALYTICS_RESET);
|
||||||
clearBrowserSessionCookies();
|
clearBrowserSessionCookies();
|
||||||
|
clearLocalStorageOnLogout();
|
||||||
const globalConfig = window.globalConfig || {};
|
const globalConfig = window.globalConfig || {};
|
||||||
const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/';
|
const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/';
|
||||||
window.location = logoutRedirectLink;
|
window.location = logoutRedirectLink;
|
||||||
|
|
Loading…
Reference in a new issue