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:
Nithin David Thomas 2022-10-04 09:59:02 +05:30 committed by GitHub
parent beedfc47bf
commit 8df7547043
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 1 deletions

View file

@ -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;

View file

@ -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,

View file

@ -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;