From 8df7547043f699acf343259b1dd1b2f8882556b4 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Tue, 4 Oct 2022 09:59:02 +0530 Subject: [PATCH] feat: Add support for draft messages in reply box (#4440) Co-authored-by: Pranav Raj S 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 --- .../widgets/conversation/ReplyBox.vue | 87 +++++++++++++++++++ .../dashboard/store/modules/auth.js | 8 +- app/javascript/dashboard/store/utils/api.js | 6 ++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 07e0cf773..1b0b9ca36 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -147,6 +147,7 @@ import { MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL, } from 'shared/constants/messages'; import { BUS_EVENTS } from 'shared/constants/busEvents'; + import WhatsappTemplates from './WhatsappTemplates/Modal.vue'; import { buildHotKeys } from 'shared/helpers/KeyboardHelpers'; 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 { DirectUpload } from 'activestorage'; 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 { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings'; @@ -209,6 +212,7 @@ export default { hasSlashCommand: false, bccEmails: '', ccEmails: '', + doAutoSaveDraft: () => {}, showWhatsAppTemplatesModal: false, }; }, @@ -434,6 +438,13 @@ export default { enterToSendEnabled() { return this.editorMessageKey === 'enter'; }, + conversationId() { + return this.currentChat.id; + }, + conversationIdByRoute() { + const { conversation_id: conversationId } = this.$route.params; + return conversationId; + }, editorStateId() { return `draft-${this.conversationIdByRoute}-${this.replyType}`; }, @@ -441,6 +452,7 @@ export default { watch: { currentChat(conversation) { const { can_reply: canReply } = conversation; + if (this.isOnPrivateNote) { return; } @@ -453,6 +465,12 @@ export default { this.setCCEmailFromLastChat(); }, + conversationIdByRoute(conversationId, oldConversationId) { + if (conversationId !== oldConversationId) { + this.setToDraft(oldConversationId, this.replyType); + this.getFromDraft(); + } + }, message(updatedMessage) { this.hasSlashCommand = updatedMessage[0] === '/' && !this.showRichContentEditor; @@ -465,21 +483,86 @@ export default { this.mentionSearchKey = ''; this.showMentions = false; } + this.doAutoSaveDraft(); + }, + replyType(updatedReplyType, oldReplyType) { + this.setToDraft(this.conversationIdByRoute, oldReplyType); + this.getFromDraft(); }, }, mounted() { + this.getFromDraft(); // Donot use the keyboard listener mixin here as the events here are supposed to be // working even if input/textarea is focussed. document.addEventListener('paste', this.onPaste); document.addEventListener('keydown', this.handleKeyEvents); this.setCCEmailFromLastChat(); + this.doAutoSaveDraft = debounce( + () => { + this.saveDraft(this.conversationIdByRoute, this.replyType); + }, + 500, + true + ); }, destroyed() { document.removeEventListener('paste', this.onPaste); document.removeEventListener('keydown', this.handleKeyEvents); }, 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) { const keyCode = buildHotKeys(e); if (keyCode === 'escape') { @@ -564,11 +647,13 @@ export default { newMessage += '\n\n' + this.messageSignature; } const messagePayload = this.getMessagePayload(newMessage); + this.clearMessage(); if (!this.isPrivate) { this.clearEmailField(); } this.sendMessage(messagePayload); + this.clearMessage(); this.hideEmojiPicker(); this.$emit('update:popoutReplyBox', false); } @@ -580,6 +665,7 @@ export default { messagePayload ); bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE); + this.removeFromDraft(); } catch (error) { const errorMessage = error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR'); @@ -658,6 +744,7 @@ export default { }, onBlur() { this.isFocused = false; + this.saveDraft(this.conversationIdByRoute, this.replyType); }, onFocus() { this.isFocused = true; diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index 4d33dd0cc..16bd0e4bb 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -1,7 +1,12 @@ import Vue from 'vue'; import types from '../mutation-types'; import authAPI from '../../api/auth'; -import { setUser, clearCookiesOnLogout } from '../utils/api'; + +import { + setUser, + clearCookiesOnLogout, + clearLocalStorageOnLogout, +} from '../utils/api'; import { getLoginRedirectURL } from '../../helper/URLHelper'; const initialState = { @@ -89,6 +94,7 @@ export const actions = { authAPI .login(credentials) .then(response => { + clearLocalStorageOnLogout(); window.location = getLoginRedirectURL({ ssoAccountId, ssoConversationId, diff --git a/app/javascript/dashboard/store/utils/api.js b/app/javascript/dashboard/store/utils/api.js index 292ebca7e..93791b760 100644 --- a/app/javascript/dashboard/store/utils/api.js +++ b/app/javascript/dashboard/store/utils/api.js @@ -7,6 +7,7 @@ import { CHATWOOT_RESET, CHATWOOT_SET_USER, } from '../../helper/scriptHelpers'; +import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../helper/localStorage'; Cookies.defaults = { sameSite: 'Lax' }; @@ -37,10 +38,15 @@ export const clearBrowserSessionCookies = () => { Cookies.remove('user'); }; +export const clearLocalStorageOnLogout = () => { + LocalStorage.remove(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES); +}; + export const clearCookiesOnLogout = () => { window.bus.$emit(CHATWOOT_RESET); window.bus.$emit(ANALYTICS_RESET); clearBrowserSessionCookies(); + clearLocalStorageOnLogout(); const globalConfig = window.globalConfig || {}; const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/'; window.location = logoutRedirectLink;