From 6b53b09fb53cdf911a0d4645d43f491ac1bd5d40 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 18 Oct 2023 16:33:40 +0200 Subject: [PATCH 1/4] render caption as part of the message Signed-off-by: Maksim Sukharev --- .../MessagesGroup/Message/Message.spec.js | 33 ++++++++++++++++++- .../MessagesGroup/Message/Message.vue | 21 ++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js index 232c7aeda1d..46bf01aa579 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js @@ -325,6 +325,7 @@ describe('Message.vue', () => { const messageEl = wrapper.findComponent({ name: 'NcRichText' }) // note: indices as object keys are on purpose expect(messageEl.props('arguments')).toMatchObject(expectedRichParameters) + return messageEl } test('renders mentions', () => { @@ -364,7 +365,7 @@ describe('Message.vue', () => { ) }) - test('renders file previews', () => { + test('renders single file preview', () => { const params = { actor: { id: 'alice', @@ -391,6 +392,36 @@ describe('Message.vue', () => { ) }) + test('renders single file preview with caption', () => { + const caption = 'text caption' + const params = { + actor: { + id: 'alice', + name: 'Alice', + type: 'user', + }, + file: { + path: 'some/path', + type: 'file', + }, + } + const messageEl = renderRichObject( + caption, + params, { + actor: { + component: Mention, + props: params.actor, + }, + file: { + component: FilePreview, + props: params.file, + }, + } + ) + + expect(messageEl.props('text')).toBe('{file}' + '\n\n' + caption) + }) + test('renders deck cards', () => { const params = { actor: { diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue index 1032bcab5ff..68e62769a0a 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.vue +++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue @@ -47,11 +47,11 @@ the main body of the message as well as a quote. class="message-body__main__text">
- {{ message }} + {{ renderedMessage }}
-
-
- - Date: Wed, 18 Oct 2023 16:37:12 +0200 Subject: [PATCH 2/4] replace action buttons with NewMessage in upload editor Signed-off-by: Maksim Sukharev --- src/components/NewMessage/NewMessage.vue | 31 ++++++++-- .../NewMessage/NewMessageUploadEditor.vue | 56 ++++++++++--------- src/store/fileUploadStore.js | 6 +- 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue index dd9ef675340..ab4fd1639ca 100644 --- a/src/components/NewMessage/NewMessage.vue +++ b/src/components/NewMessage/NewMessage.vue @@ -109,7 +109,7 @@ @@ -83,6 +85,7 @@ import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' import AudioPlayer from '../MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue' import FilePreview from '../MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue' +import NewMessage from './NewMessage.vue' import TransitionWrapper from '../TransitionWrapper.vue' export default { @@ -94,9 +97,16 @@ export default { Plus, AudioPlayer, NcButton, + NewMessage, TransitionWrapper, }, + data() { + return { + modalContainerId: null, + } + }, + computed: { token() { return this.$store.getters.getToken() @@ -129,8 +139,7 @@ export default { return this.files[Object.keys(this.files)[0]] }, - // Hide the plus button in case this editor is used while sending a voice - // message + // Hide the plus button in case this editor is used while sending a voice message isVoiceMessage() { if (!this.firstFile) { return false @@ -154,17 +163,21 @@ export default { }, watch: { - showModal(show) { + async showModal(show) { if (show) { - this.focus() + await this.getContainerId() + this.$nextTick(() => { + this.$refs.newMessage?.focusInput() + }) } }, }, methods: { - focus() { + async getContainerId() { this.$nextTick(() => { - this.$refs.submitButton.$el.focus() + // Postpone render of NewMessage until modal container is mounted + this.modalContainerId = `#modal-description-${this.$refs.modal.randId}` }) }, @@ -172,8 +185,8 @@ export default { this.$store.dispatch('discardUpload', this.currentUploadId) }, - handleUpload() { - this.$store.dispatch('uploadFiles', this.currentUploadId) + handleUpload(caption) { + this.$store.dispatch('uploadFiles', { uploadId: this.currentUploadId, caption }) }, /** * Clicks the hidden file input when clicking the correspondent NcActionButton, @@ -204,21 +217,11 @@ export default { padding: 16px; &__previews { - overflow-x: hidden !important; display: flex; position: relative; overflow: auto; flex-wrap: wrap; } - &__actions { - display: flex; - justify-content: space-between; - margin-top: 16px; - margin-bottom: 4px; - button { - margin: 0 4px 0 4px; - } - } } .add-more { @@ -230,5 +233,4 @@ export default { margin: auto; } } - diff --git a/src/store/fileUploadStore.js b/src/store/fileUploadStore.js index fee6ec55951..305e0e67a81 100644 --- a/src/store/fileUploadStore.js +++ b/src/store/fileUploadStore.js @@ -282,9 +282,11 @@ const actions = { * @param {Function} context.dispatch the contexts dispatch function. * @param {object} context.getters the contexts getters object. * @param {object} context.state the contexts state object. - * @param {string} uploadId The unique uploadId + * @param {object} data the wrapping object + * @param {string} data.uploadId The unique uploadId + * @param {string} [data.caption] The text caption to the media */ - async uploadFiles({ commit, dispatch, state, getters }, uploadId) { + async uploadFiles({ commit, dispatch, state, getters }, { uploadId, caption }) { if (state.currentUploadId === uploadId) { commit('setCurrentUploadId', undefined) } From 9fc9d065a4ee034f0c27fdb487f4109711ef1c6c Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 18 Oct 2023 16:59:34 +0200 Subject: [PATCH 3/4] upload shared files in parallel, pass caption to server with the last file Signed-off-by: Maksim Sukharev --- .../NewMessage/NewMessageUploadEditor.vue | 11 +- src/store/fileUploadStore.js | 104 ++++++++---------- src/store/fileUploadStore.spec.js | 94 +++++++++++----- 3 files changed, 116 insertions(+), 93 deletions(-) diff --git a/src/components/NewMessage/NewMessageUploadEditor.vue b/src/components/NewMessage/NewMessageUploadEditor.vue index 86b9a020ccb..b0d687bba1a 100644 --- a/src/components/NewMessage/NewMessageUploadEditor.vue +++ b/src/components/NewMessage/NewMessageUploadEditor.vue @@ -40,9 +40,9 @@ tag="div" group> @@ -117,10 +117,7 @@ export default { }, files() { - if (this.currentUploadId) { - return this.$store.getters.getInitialisedUploads(this.currentUploadId) - } - return [] + return this.$store.getters.getInitialisedUploads(this.currentUploadId) }, showModal() { @@ -136,7 +133,7 @@ export default { }, firstFile() { - return this.files[Object.keys(this.files)[0]] + return this.files?.at(0)?.at(1) }, // Hide the plus button in case this editor is used while sending a voice message diff --git a/src/store/fileUploadStore.js b/src/store/fileUploadStore.js index 305e0e67a81..228cca50be8 100644 --- a/src/store/fileUploadStore.js +++ b/src/store/fileUploadStore.js @@ -48,36 +48,24 @@ const state = { const getters = { - getInitialisedUploads: (state) => (uploadId) => { + getUploadsArray: (state) => (uploadId) => { if (state.uploads[uploadId]) { - const initialisedUploads = {} - for (const index in state.uploads[uploadId].files) { - const currentFile = state.uploads[uploadId].files[index] - if (currentFile.status === 'initialised') { - initialisedUploads[index] = (currentFile) - } - } - return initialisedUploads + return Object.entries(state.uploads[uploadId].files) } else { - return {} + return [] } }, + getInitialisedUploads: (state, getters) => (uploadId) => { + return getters.getUploadsArray(uploadId) + .filter(([_index, uploadedFile]) => uploadedFile.status === 'initialised') + }, + // Returns all the files that have been successfully uploaded provided an // upload id - getShareableFiles: (state) => (uploadId) => { - if (state.uploads[uploadId]) { - const shareableFiles = {} - for (const index in state.uploads[uploadId].files) { - const currentFile = state.uploads[uploadId].files[index] - if (currentFile.status === 'successUpload') { - shareableFiles[index] = (currentFile) - } - } - return shareableFiles - } else { - return {} - } + getShareableFiles: (state, getters) => (uploadId) => { + return getters.getUploadsArray(uploadId) + .filter(([_index, uploadedFile]) => uploadedFile.status === 'successUpload') }, // gets the current attachment folder @@ -293,22 +281,27 @@ const actions = { EventBus.$emit('upload-start') - // Tag the previously indexed files and add the temporary messages to the - // messages list - for (const index in state.uploads[uploadId].files) { + // Tag previously indexed files and add temporary messages to the MessagesList + // If caption is provided, attach to the last temporary message + const lastIndex = getters.getUploadsArray(uploadId).at(-1).at(0) + for (const [index, uploadedFile] of getters.getUploadsArray(uploadId)) { // mark all files as uploading commit('markFileAsUploading', { uploadId, index }) // Store the previously created temporary message - const temporaryMessage = state.uploads[uploadId].files[index].temporaryMessage + const temporaryMessage = { + ...uploadedFile.temporaryMessage, + message: index === lastIndex ? caption : '{file}', + } // Add temporary messages (files) to the messages list dispatch('addTemporaryMessage', temporaryMessage) // Scroll the message list EventBus.$emit('scroll-chat-to-bottom') } + // Iterate again and perform the uploads - for (const index in state.uploads[uploadId].files) { + await Promise.allSettled(getters.getUploadsArray(uploadId).map(async ([index, uploadedFile]) => { // currentFile to be uploaded - const currentFile = state.uploads[uploadId].files[index].file + const currentFile = uploadedFile.file // userRoot path const userRoot = '/files/' + getters.getUserId() const fileName = (currentFile.newName || currentFile.name) @@ -345,41 +338,34 @@ const actions = { showError(t('spreed', 'Error while uploading file "{fileName}"', { fileName })) } - const temporaryMessage = state.uploads[uploadId].files[index].temporaryMessage // Mark the upload as failed in the store commit('markFileAsFailedUpload', { uploadId, index }) - dispatch('markTemporaryMessageAsFailed', { - message: temporaryMessage, - reason, - }) + dispatch('markTemporaryMessageAsFailed', { message: uploadedFile.temporaryMessage, reason }) } - - // Get the files that have successfully been uploaded from the store - const shareableFiles = getters.getShareableFiles(uploadId) - // Share each of those files to the conversation - for (const index in shareableFiles) { - const path = shareableFiles[index].sharePath - const temporaryMessage = shareableFiles[index].temporaryMessage - const metadata = JSON.stringify({ messageType: temporaryMessage.messageType }) - try { - const token = temporaryMessage.token - dispatch('markFileAsSharing', { uploadId, index }) - await shareFile(path, token, temporaryMessage.referenceId, metadata) - dispatch('markFileAsShared', { uploadId, index }) - } catch (error) { - if (error?.response?.status === 403) { - showError(t('spreed', 'You are not allowed to share files')) - } else { - showError(t('spreed', 'An error happened when trying to share your file')) - } - dispatch('markTemporaryMessageAsFailed', { - message: temporaryMessage, - reason: 'failed-share', - }) - console.error('An error happened when trying to share your file: ', error) + })) + + // Share the files, that have successfully been uploaded from the store, to the conversation + await Promise.all(getters.getShareableFiles(uploadId).map(async ([index, shareableFile]) => { + const path = shareableFile.sharePath + const temporaryMessage = shareableFile.temporaryMessage + const metadata = (caption && index === lastIndex) + ? JSON.stringify({ messageType: temporaryMessage.messageType, caption }) + : JSON.stringify({ messageType: temporaryMessage.messageType }) + try { + const token = temporaryMessage.token + dispatch('markFileAsSharing', { uploadId, index }) + await shareFile(path, token, temporaryMessage.referenceId, metadata) + dispatch('markFileAsShared', { uploadId, index }) + } catch (error) { + if (error?.response?.status === 403) { + showError(t('spreed', 'You are not allowed to share files')) + } else { + showError(t('spreed', 'An error happened when trying to share your file')) } + dispatch('markTemporaryMessageAsFailed', { message: temporaryMessage, reason: 'failed-share' }) + console.error('An error happened when trying to share your file: ', error) } - } + })) EventBus.$emit('upload-finished') }, /** diff --git a/src/store/fileUploadStore.spec.js b/src/store/fileUploadStore.spec.js index c35c9947250..cf1762f6e41 100644 --- a/src/store/fileUploadStore.spec.js +++ b/src/store/fileUploadStore.spec.js @@ -111,6 +111,8 @@ describe('fileUploadStore', () => { lastModified: Date.UTC(2021, 3, 25, 15, 30, 0), }, ] + const localUrls = ['local-url:pngimage.png', 'local-url:jpgimage.jpg', 'icon-url:text/plain'] + await store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', @@ -118,22 +120,58 @@ describe('fileUploadStore', () => { }) const uploads = store.getters.getInitialisedUploads('upload-id1') - expect(Object.keys(uploads).length).toBe(3) - - for (let i = 0; i < files.length; i++) { - expect(mockedActions.createTemporaryMessage.mock.calls[i][1].text).toBe('{file}') - expect(mockedActions.createTemporaryMessage.mock.calls[i][1].uploadId).toBe('upload-id1') - expect(mockedActions.createTemporaryMessage.mock.calls[i][1].index).toBeDefined() - expect(mockedActions.createTemporaryMessage.mock.calls[i][1].file).toBe(files[i]) - expect(mockedActions.createTemporaryMessage.mock.calls[i][1].token).toBe('XXTOKENXX') + expect(uploads).toHaveLength(files.length) + + for (const index in files) { + expect(mockedActions.createTemporaryMessage.mock.calls[index][1]).toMatchObject({ + text: '{file}', + token: 'XXTOKENXX', + uploadId: 'upload-id1', + index: expect.anything(), + file: files[index], + localUrl: localUrls[index], + }) + } + }) + + test('performs upload and sharing of single file', async () => { + const file = { + name: 'pngimage.png', + type: 'image/png', + size: 123, + lastModified: Date.UTC(2021, 3, 27, 15, 30, 0), } + const fileBuffer = await new Blob([file]).arrayBuffer() + + await store.dispatch('initialiseUpload', { + uploadId: 'upload-id1', + token: 'XXTOKENXX', + files: [file], + }) + + expect(store.getters.currentUploadId).toBe('upload-id1') - expect(mockedActions.createTemporaryMessage.mock.calls[0][1].localUrl).toBe('local-url:pngimage.png') - expect(mockedActions.createTemporaryMessage.mock.calls[1][1].localUrl).toBe('local-url:jpgimage.jpg') - expect(mockedActions.createTemporaryMessage.mock.calls[2][1].localUrl).toBe('icon-url:text/plain') + const uniqueFileName = '/Talk/' + file.name + 'uniq' + findUniquePath.mockResolvedValueOnce(uniqueFileName) + client.putFileContents.mockResolvedValue() + shareFile.mockResolvedValue() + + await store.dispatch('uploadFiles', { uploadId: 'upload-id1', caption: 'text-caption' }) + + expect(findUniquePath).toHaveBeenCalledTimes(1) + expect(findUniquePath).toHaveBeenCalledWith(client, '/files/current-user', '/Talk/' + file.name) + + expect(client.putFileContents).toHaveBeenCalledTimes(1) + expect(client.putFileContents).toHaveBeenCalledWith(`/files/current-user${uniqueFileName}`, fileBuffer, expect.anything()) + + expect(shareFile).toHaveBeenCalledTimes(1) + expect(shareFile).toHaveBeenCalledWith(`/${uniqueFileName}`, 'XXTOKENXX', 'reference-id-1', '{"caption":"text-caption"}') + + expect(mockedActions.addTemporaryMessage).toHaveBeenCalledTimes(1) + expect(store.getters.currentUploadId).not.toBeDefined() }) - test('performs upload by uploading then sharing', async () => { + test('performs upload and sharing of multiple files', async () => { const file1 = { name: 'pngimage.png', type: 'image/png', @@ -164,23 +202,25 @@ describe('fileUploadStore', () => { .mockResolvedValueOnce('/Talk/' + files[0].name + 'uniq') .mockResolvedValueOnce('/Talk/' + files[1].name + 'uniq') client.putFileContents.mockResolvedValue() - shareFile.mockResolvedValue() + shareFile + .mockResolvedValueOnce({ data: { ocs: { data: { id: '1' } } } }) + .mockResolvedValueOnce({ data: { ocs: { data: { id: '2' } } } }) - await store.dispatch('uploadFiles', 'upload-id1') + await store.dispatch('uploadFiles', { uploadId: 'upload-id1', caption: 'text-caption' }) + expect(findUniquePath).toHaveBeenCalledTimes(2) expect(client.putFileContents).toHaveBeenCalledTimes(2) expect(shareFile).toHaveBeenCalledTimes(2) - for (let i = 0; i < files.length; i++) { - expect(findUniquePath).toHaveBeenCalledWith(client, '/files/current-user', '/Talk/' + files[i].name) - expect(client.putFileContents.mock.calls[i][0]).toBe('/files/current-user/Talk/' + files[i].name + 'uniq') - expect(client.putFileContents.mock.calls[i][1]).toStrictEqual(fileBuffers[i]) - - expect(shareFile.mock.calls[i][0]).toBe('//Talk/' + files[i].name + 'uniq') - expect(shareFile.mock.calls[i][1]).toBe('XXTOKENXX') - expect(shareFile.mock.calls[i][2]).toBe('reference-id-' + (i + 1)) + for (const index in files) { + expect(findUniquePath).toHaveBeenCalledWith(client, '/files/current-user', '/Talk/' + files[index].name) + expect(client.putFileContents).toHaveBeenCalledWith(`/files/current-user/Talk/${files[index].name}uniq`, fileBuffers[index], expect.anything()) } + expect(shareFile).toHaveBeenCalledTimes(2) + expect(shareFile).toHaveBeenNthCalledWith(1, '//Talk/' + files[0].name + 'uniq', 'XXTOKENXX', 'reference-id-1', '{}') + expect(shareFile).toHaveBeenNthCalledWith(2, '//Talk/' + files[1].name + 'uniq', 'XXTOKENXX', 'reference-id-2', '{"caption":"text-caption"}') + expect(mockedActions.addTemporaryMessage).toHaveBeenCalledTimes(2) expect(store.getters.currentUploadId).not.toBeDefined() }) @@ -209,7 +249,7 @@ describe('fileUploadStore', () => { }, }) - await store.dispatch('uploadFiles', 'upload-id1') + await store.dispatch('uploadFiles', { uploadId: 'upload-id1' }) expect(client.putFileContents).toHaveBeenCalledTimes(1) expect(shareFile).not.toHaveBeenCalled() @@ -247,7 +287,7 @@ describe('fileUploadStore', () => { }, }) - await store.dispatch('uploadFiles', 'upload-id1') + await store.dispatch('uploadFiles', { uploadId: 'upload-id1' }) expect(client.putFileContents).toHaveBeenCalledTimes(1) expect(shareFile).toHaveBeenCalledTimes(1) @@ -286,9 +326,9 @@ describe('fileUploadStore', () => { await store.dispatch('removeFileFromSelection', 2) const uploads = store.getters.getInitialisedUploads('upload-id1') - expect(Object.keys(uploads).length).toBe(1) + expect(uploads).toHaveLength(1) - expect(Object.values(uploads)[0].file).toBe(files[0]) + expect(uploads[0][1].file).toBe(files[0]) }) test('discard an entire upload', async () => { @@ -316,7 +356,7 @@ describe('fileUploadStore', () => { await store.dispatch('discardUpload', 'upload-id1') const uploads = store.getters.getInitialisedUploads('upload-id1') - expect(uploads).toStrictEqual({}) + expect(uploads).toStrictEqual([]) expect(store.getters.currentUploadId).not.toBeDefined() }) From 892c22b7e29ebf0248f143adcc07eb3ea71bf8ce Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Fri, 20 Oct 2023 20:28:02 +0200 Subject: [PATCH 4/4] use local urls (blob) in file previews to not wait for server Signed-off-by: Maksim Sukharev --- .../MessagesGroup/Message/Message.vue | 6 ++++++ .../Message/MessagePart/FilePreview.vue | 16 +++++++++++++++- src/store/fileUploadStore.js | 17 +++++++++++------ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue index 68e62769a0a..c282e823a46 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.vue +++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue @@ -457,6 +457,11 @@ export default { type: Array, default: () => { return [] }, }, + + referenceId: { + type: String, + default: '', + }, }, emits: ['toggle-combined-system-message'], @@ -609,6 +614,7 @@ export default { props: Object.assign({ token: this.token, itemType, + referenceId: this.referenceId, }, this.messageParameters[p]), } } else if (type === 'deck-card') { diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index b2e76a1fa1e..ced1222d7a2 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -32,7 +32,7 @@ 'file-preview--row-layout': rowLayout }" @click.exact="handleClick" @keydown.enter="handleClick"> -
@@ -121,6 +121,13 @@ export default { type: String, required: true, }, + /** + * Reference id from the message + */ + referenceId: { + type: String, + default: '', + }, /** * File name */ @@ -283,6 +290,10 @@ export default { return this.name }, + fallbackLocalUrl() { + return this.$store.getters.getLocalUrl(this.referenceId) + }, + previewTooltip() { if (this.shouldShowFileDetail) { // no tooltip as the file name is already visible directly @@ -363,6 +374,9 @@ export default { if (this.previewType === PREVIEW_TYPE.TEMPORARY) { return this.localUrl } + if (this.fallbackLocalUrl) { + return this.fallbackLocalUrl + } if (this.previewType === PREVIEW_TYPE.MIME_ICON || this.rowLayout) { return OC.MimeType.getIconUrl(this.mimetype) } diff --git a/src/store/fileUploadStore.js b/src/store/fileUploadStore.js index 228cca50be8..a167927bff0 100644 --- a/src/store/fileUploadStore.js +++ b/src/store/fileUploadStore.js @@ -38,10 +38,9 @@ import { findUniquePath, getFileExtension } from '../utils/fileUpload.js' const state = { attachmentFolder: loadState('spreed', 'attachment_folder', ''), attachmentFolderFreeSpace: loadState('spreed', 'attachment_folder_free_space', 0), - uploads: { - }, + uploads: {}, currentUploadId: undefined, - + localUrls: {}, fileTemplatesInitialised: false, fileTemplates: [], } @@ -78,6 +77,11 @@ const getters = { return state.attachmentFolderFreeSpace }, + // returns the local Url of uploaded image + getLocalUrl: (state) => (referenceId) => { + return state.localUrls[referenceId] + }, + uploadProgress: (state) => (uploadId, index) => { if (state.uploads[uploadId].files[index]) { return state.uploads[uploadId].files[index].uploadedSize / state.uploads[uploadId].files[index].totalSize * 100 @@ -102,7 +106,7 @@ const getters = { const mutations = { // Adds a "file to be shared to the store" - addFileToBeUploaded(state, { file, temporaryMessage }) { + addFileToBeUploaded(state, { file, temporaryMessage, localUrl }) { const uploadId = temporaryMessage.messageParameters.file.uploadId const token = temporaryMessage.messageParameters.file.token const index = temporaryMessage.messageParameters.file.index @@ -120,6 +124,7 @@ const mutations = { uploadedSize: 0, temporaryMessage, }) + Vue.set(state.localUrls, temporaryMessage.referenceId, localUrl) }, // Marks a given file as failed upload @@ -242,7 +247,7 @@ const actions = { text: '{file}', token, uploadId, index, file, localUrl, isVoiceMessage, }) console.debug('temporarymessage: ', temporaryMessage, 'uploadId', uploadId) - commit('addFileToBeUploaded', { file, temporaryMessage }) + commit('addFileToBeUploaded', { file, temporaryMessage, localUrl }) } }, @@ -295,7 +300,7 @@ const actions = { // Add temporary messages (files) to the messages list dispatch('addTemporaryMessage', temporaryMessage) // Scroll the message list - EventBus.$emit('scroll-chat-to-bottom') + EventBus.$emit('scroll-chat-to-bottom', { force: true }) } // Iterate again and perform the uploads