From d6a4cdf86000312ef8951e3da97e7c55681f466f Mon Sep 17 00:00:00 2001 From: DorraJaouad Date: Tue, 27 Feb 2024 15:13:17 +0100 Subject: [PATCH] Feat(draft chat input): Store input in browser storage Signed-off-by: DorraJaouad --- src/components/NewMessage/NewMessage.vue | 49 ++++++++++++++---------- src/store/conversationsStore.spec.js | 1 + src/stores/__tests__/chatExtras.spec.js | 21 ++++++++++ src/stores/chatExtras.js | 30 +++++++++++++-- 4 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue index 7fafebcd01cb..595c563191fd 100644 --- a/src/components/NewMessage/NewMessage.vue +++ b/src/components/NewMessage/NewMessage.vue @@ -227,6 +227,7 @@ import { useSettingsStore } from '../../stores/settings.js' import { fetchClipboardContent } from '../../utils/clipboard.js' import { isDarkTheme } from '../../utils/isDarkTheme.js' import { parseSpecialSymbols } from '../../utils/textParse.js' +import debounce from 'debounce' const disableKeyboardShortcuts = OCP.Accessibility.disableKeyboardShortcuts() const supportTypingStatus = getCapabilities()?.spreed?.config?.chat?.['typing-privacy'] !== undefined @@ -333,6 +334,7 @@ export default { clipboardTimeStamp: null, typingInterval: null, wasTypingWithinInterval: false, + debouncedUpdateChatInput: debounce(this.updateChatInput, 50) } }, @@ -459,6 +461,10 @@ export default { return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) }, + chatInput() { + return this.chatExtrasStore.getChatInput(this.token) + }, + chatEditInput() { return this.chatExtrasStore.getChatEditInput(this.token) }, @@ -475,15 +481,7 @@ export default { }, text(newValue) { - if (this.messageToEdit) { - this.chatExtrasStore.setChatEditInput({ - token: this.token, - text: newValue, - parameters: this.messageToEdit.messageParameters - }) - } else { - this.chatExtrasStore.setChatInput({ token: this.token, text: newValue }) - } + this.debouncedUpdateChatInput(newValue) }, messageToEdit(newValue) { @@ -491,7 +489,7 @@ export default { this.text = this.chatExtrasStore.getChatEditInput(this.token) this.chatExtrasStore.removeParentIdToReply(this.token) } else { - this.text = this.chatExtrasStore.getChatInput(this.token) + this.text = this.chatInput } }, @@ -512,8 +510,8 @@ export default { handler(token) { if (token) { this.text = this.messageToEdit - ? this.chatExtrasStore.getChatEditInput(token) - : this.chatExtrasStore.getChatInput(token) + ? this.chatEditInput + : this.chatInput } else { this.text = '' } @@ -529,7 +527,6 @@ export default { EventBus.$on('upload-start', this.handleUploadSideEffects) EventBus.$on('upload-discard', this.handleUploadSideEffects) EventBus.$on('retry-message', this.handleRetryMessage) - this.text = this.chatExtrasStore.getChatInput(this.token) if (!this.$store.getters.areFileTemplatesInitialised) { this.$store.dispatch('getFileTemplates') @@ -581,13 +578,25 @@ export default { } }, + updateChatInput(text) { + if (this.messageToEdit) { + this.chatExtrasStore.setChatEditInput({ + token: this.token, + text, + parameters: this.messageToEdit.messageParameters + }) + } else if (text && text !== this.chatInput) { + this.chatExtrasStore.setChatInput({ token: this.token, text}) + } else if (!text && this.chatInput) { + this.chatExtrasStore.removeChatInput(this.token) + } + }, + handleUploadSideEffects() { if (this.upload) { return } this.$nextTick(() => { - // reset or fill main input in chat view from the store - this.text = this.chatExtrasStore.getChatInput(this.token) // refocus input as the user might want to type further this.focusInput() }) @@ -622,11 +631,11 @@ export default { this.text = parseSpecialSymbols(this.text) } - if (this.upload) { - // Clear input content from store and remove Quote component - this.chatExtrasStore.setChatInput({ token: this.token, text: '' }) - this.chatExtrasStore.removeParentIdToReply(this.token) + // Clear input content from store and remove Quote component + this.chatExtrasStore.removeParentIdToReply(this.token) + this.chatExtrasStore.removeChatInput(this.token) + if (this.upload) { if (this.$store.getters.getInitialisedUploads(this.$store.getters.currentUploadId).length) { // If dialog contains files to upload, delegate sending this.$emit('upload', { caption: this.text, options }) @@ -646,8 +655,6 @@ export default { this.userData = {} // Scrolls the message list to the last added message EventBus.$emit('smooth-scroll-chat-to-bottom') - // Also remove the message to be replied for this conversation - this.chatExtrasStore.removeParentIdToReply(this.token) this.broadcast ? await this.broadcastMessage(this.token, temporaryMessage.message) diff --git a/src/store/conversationsStore.spec.js b/src/store/conversationsStore.spec.js index 40977145ba32..23b4dd215e6e 100644 --- a/src/store/conversationsStore.spec.js +++ b/src/store/conversationsStore.spec.js @@ -63,6 +63,7 @@ jest.mock('@nextcloud/event-bus') jest.mock('../services/BrowserStorage.js', () => ({ getItem: jest.fn(), setItem: jest.fn(), + removeItem: jest.fn(), })) describe('conversationsStore', () => { diff --git a/src/stores/__tests__/chatExtras.spec.js b/src/stores/__tests__/chatExtras.spec.js index 6596fc710a5a..98841b4b33c1 100644 --- a/src/stores/__tests__/chatExtras.spec.js +++ b/src/stores/__tests__/chatExtras.spec.js @@ -1,5 +1,6 @@ import { setActivePinia, createPinia } from 'pinia' +import BrowserStorage from '../../services/BrowserStorage.js' import { EventBus } from '../../services/EventBus.js' import { getUserAbsence } from '../../services/participantsService.js' import { generateOCSErrorResponse, generateOCSResponse } from '../../test-helpers.js' @@ -103,6 +104,7 @@ describe('chatExtrasStore', () => { // Assert expect(chatExtrasStore.getChatInput('token-1')).toStrictEqual('message-1') + expect(BrowserStorage.getItem('chatInput_token-1')).toBe('message-1') }) it('clears current input message', () => { @@ -115,6 +117,25 @@ describe('chatExtrasStore', () => { // Assert expect(chatExtrasStore.chatInput['token-1']).not.toBeDefined() expect(chatExtrasStore.getChatInput('token-1')).toBe('') + expect(BrowserStorage.getItem('chatInput_token-1')).toBe(null) + }) + + it('restores chat input from the browser storage if any', () => { + // Arrange + BrowserStorage.setItem('chatInput_token-1', 'message draft') + + // Act + chatExtrasStore.restoreChatInput('token-1') + + // Assert + expect(chatExtrasStore.getChatInput('token-1')).toStrictEqual('message draft') + + // Arrange 2 - no chat input in the browser storage + chatExtrasStore.removeChatInput('token-1') + // Act + chatExtrasStore.restoreChatInput('token-1') + // Assert + expect(chatExtrasStore.getChatInput('token-1')).toBe('') }) }) diff --git a/src/stores/chatExtras.js b/src/stores/chatExtras.js index 1d43b09688a6..57d95e4b0fcc 100644 --- a/src/stores/chatExtras.js +++ b/src/stores/chatExtras.js @@ -24,6 +24,7 @@ import { defineStore } from 'pinia' import Vue from 'vue' +import BrowserStorage from '../services/BrowserStorage.js' import { EventBus } from '../services/EventBus.js' import { getUserAbsence } from '../services/participantsService.js' import { parseSpecialSymbols, parseMentions } from '../utils/textParse.js' @@ -61,10 +62,6 @@ export const useChatExtrasStore = defineStore('chatExtras', { } }, - getChatInput: (state) => (token) => { - return state.chatInput[token] ?? '' - }, - getChatEditInput: (state) => (token) => { return state.chatEditInput[token] ?? '' }, @@ -75,6 +72,19 @@ export const useChatExtrasStore = defineStore('chatExtras', { }, actions: { + /** + * Fetch an absence status for user and save to store + * + * @param {string} token The conversation token + * @return {string} The input text + */ + getChatInput(token) { + if (!this.chatInput[token]) { + this.restoreChatInput(token) + } + return this.chatInput[token] + }, + /** * Fetch an absence status for user and save to store * @@ -130,6 +140,16 @@ export const useChatExtrasStore = defineStore('chatExtras', { Vue.delete(this.parentToReply, token) }, + /** + * Restore chat input from the browser storage and save to store + * + * @param {string} token The conversation token + */ + restoreChatInput(token) { + const chatInput = BrowserStorage.getItem('chatInput_' + token) + Vue.set(this.chatInput, token, chatInput ?? '') + }, + /** * Add a current input value to the store for a given conversation token * @@ -139,6 +159,7 @@ export const useChatExtrasStore = defineStore('chatExtras', { */ setChatInput({ token, text }) { const parsedText = parseSpecialSymbols(text) + BrowserStorage.setItem('chatInput_' + token, parsedText) Vue.set(this.chatInput, token, parsedText) }, @@ -186,6 +207,7 @@ export const useChatExtrasStore = defineStore('chatExtras', { * @param {string} token The conversation token */ removeChatInput(token) { + BrowserStorage.removeItem('chatInput_' + token) Vue.delete(this.chatInput, token) },