diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue index a8b85b48f99..4268937afcc 100644 --- a/src/components/NewMessage/NewMessage.vue +++ b/src/components/NewMessage/NewMessage.vue @@ -205,6 +205,7 @@ import PollDraftHandler from '../PollViewer/PollDraftHandler.vue' import Quote from '../Quote.vue' import { useChatMentions } from '../../composables/useChatMentions.ts' +import { useTemporaryMessage } from '../../composables/useTemporaryMessage.ts' import { CONVERSATION, PARTICIPANT, PRIVACY } from '../../constants.js' import BrowserStorage from '../../services/BrowserStorage.js' import { getTalkConfig, hasTalkFeature } from '../../services/CapabilitiesManager.ts' @@ -293,6 +294,7 @@ export default { const { token } = toRefs(props) const supportTypingStatus = getTalkConfig(token.value, 'chat', 'typing-privacy') !== undefined const { autoComplete, userData } = useChatMentions(token) + const { createTemporaryMessage } = useTemporaryMessage() return { breakoutRoomsStore: useBreakoutRoomsStore(), @@ -301,6 +303,7 @@ export default { supportTypingStatus, autoComplete, userData, + createTemporaryMessage, } }, @@ -676,8 +679,8 @@ export default { } if (this.hasText) { - const temporaryMessage = await this.$store.dispatch('createTemporaryMessage', { - text: this.text.trim(), + const temporaryMessage = this.createTemporaryMessage({ + message: this.text.trim(), token: this.token, }) this.text = '' @@ -835,7 +838,7 @@ export default { * @param {boolean} rename whether to rename the files * @param {boolean} isVoiceMessage indicates whether the file is a voice message */ - async handleFiles(files, rename = false, isVoiceMessage = false) { + handleFiles(files, rename = false, isVoiceMessage = false) { if (!this.canUploadFiles) { showWarning(t('spreed', 'File upload is not available in this conversation')) return @@ -843,7 +846,7 @@ export default { // Create a unique id for the upload operation const uploadId = this.currentUploadId ?? new Date().getTime() // Uploads and shares the files - await this.$store.dispatch('initialiseUpload', { files, token: this.token, uploadId, rename, isVoiceMessage }) + this.$store.dispatch('initialiseUpload', { files, token: this.token, uploadId, rename, isVoiceMessage }) }, /** diff --git a/src/components/NewMessage/NewMessageAudioRecorder.vue b/src/components/NewMessage/NewMessageAudioRecorder.vue index b78f5f44004..c13eb0e7c95 100644 --- a/src/components/NewMessage/NewMessageAudioRecorder.vue +++ b/src/components/NewMessage/NewMessageAudioRecorder.vue @@ -246,7 +246,7 @@ export default { // Generate file name const fileName = this.generateFileName() // Convert blob to file - const audioFile = new File([this.blob], fileName) + const audioFile = new File([this.blob], fileName, { type: 'audio/wav' }) this.$emit('audio-file', audioFile) this.$emit('recording', false) } diff --git a/src/components/NewMessage/NewMessageUploadEditor.vue b/src/components/NewMessage/NewMessageUploadEditor.vue index 2da4e82a4a7..295e92ec873 100644 --- a/src/components/NewMessage/NewMessageUploadEditor.vue +++ b/src/components/NewMessage/NewMessageUploadEditor.vue @@ -201,9 +201,9 @@ export default { this.$refs.fileUploadInput.click() }, - async handleFileInput(event) { + handleFileInput(event) { const files = Object.values(event.target.files) - await this.$store.dispatch('initialiseUpload', { files, token: this.token, uploadId: this.currentUploadId }) + this.$store.dispatch('initialiseUpload', { files, token: this.token, uploadId: this.currentUploadId }) this.$refs.fileUploadInput.value = null }, diff --git a/src/composables/useTemporaryMessage.ts b/src/composables/useTemporaryMessage.ts new file mode 100644 index 00000000000..26260e6fc78 --- /dev/null +++ b/src/composables/useTemporaryMessage.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { useStore } from './useStore.js' +import { useChatExtrasStore } from '../stores/chatExtras.js' +import { prepareTemporaryMessage } from '../utils/prepareTemporaryMessage.ts' +import type { PrepareTemporaryMessagePayload } from '../utils/prepareTemporaryMessage.ts' + +/** + * Composable to generate temporary messages using defined in store information + * @param context Vuex Store (to be used inside Vuex modules) + */ +export function useTemporaryMessage(context: unknown) { + const store = context ?? useStore() + const chatExtrasStore = useChatExtrasStore() + + /** + * @param payload payload for generating a temporary message + */ + function createTemporaryMessage(payload: PrepareTemporaryMessagePayload) { + const parentId = chatExtrasStore.getParentIdToReply(payload.token) + + return prepareTemporaryMessage({ + ...payload, + actorId: store.getters.getActorId(), + actorType: store.getters.getActorType(), + actorDisplayName: store.getters.getDisplayName(), + parent: parentId && store.getters.message(payload.token, parentId), + }) + } + + return { + createTemporaryMessage, + } +} diff --git a/src/store/fileUploadStore.js b/src/store/fileUploadStore.js index 2eee36f9ea6..754d65cd37a 100644 --- a/src/store/fileUploadStore.js +++ b/src/store/fileUploadStore.js @@ -11,6 +11,7 @@ import { t } from '@nextcloud/l10n' import moment from '@nextcloud/moment' import { getUploader } from '@nextcloud/upload' +import { useTemporaryMessage } from '../composables/useTemporaryMessage.ts' import { SHARED_ITEM } from '../constants.js' import { getDavClient } from '../services/DavClient.js' import { EventBus } from '../services/EventBus.ts' @@ -227,9 +228,10 @@ const actions = { * @param {boolean} data.rename whether to rename the files (usually after pasting) * @param {boolean} data.isVoiceMessage whether the file is a voice recording */ - async initialiseUpload({ commit, dispatch }, { uploadId, token, files, rename = false, isVoiceMessage }) { + initialiseUpload(context, { uploadId, token, files, rename = false, isVoiceMessage }) { // Set last upload id - commit('setCurrentUploadId', uploadId) + context.commit('setCurrentUploadId', uploadId) + const { createTemporaryMessage } = useTemporaryMessage(context) for (let i = 0; i < files.length; i++) { const file = files[i] @@ -249,11 +251,17 @@ const actions = { const date = new Date() const index = 'temp_' + date.getTime() + Math.random() // Create temporary message for the file and add it to the message list - const temporaryMessage = await dispatch('createTemporaryMessage', { - text: '{file}', token, uploadId, index, file, localUrl, isVoiceMessage, + const temporaryMessage = createTemporaryMessage({ + message: '{file}', + token, + uploadId, + index, + file, + localUrl, + messageType: isVoiceMessage ? 'voice-message' : 'comment', }) console.debug('temporarymessage: ', temporaryMessage, 'uploadId', uploadId) - commit('addFileToBeUploaded', { file, temporaryMessage, localUrl, token }) + context.commit('addFileToBeUploaded', { file, temporaryMessage, localUrl, token }) } }, @@ -442,7 +450,8 @@ const actions = { const [index, shareableFile] = share const { id, messageType, parent, referenceId } = shareableFile.temporaryMessage || {} - const metadata = JSON.stringify(Object.assign({ messageType }, + const metadata = JSON.stringify(Object.assign( + messageType !== 'comment' ? { messageType } : {}, caption && index === lastIndex ? { caption } : {}, options?.silent ? { silent: options.silent } : {}, parent ? { replyTo: parent.id } : {}, diff --git a/src/store/fileUploadStore.spec.js b/src/store/fileUploadStore.spec.js index 6dcf8fb1929..37e75ab8332 100644 --- a/src/store/fileUploadStore.spec.js +++ b/src/store/fileUploadStore.spec.js @@ -44,31 +44,11 @@ describe('fileUploadStore', () => { let mockedActions = null beforeEach(() => { - let temporaryMessageCount = 0 - localVue = createLocalVue() localVue.use(Vuex) setActivePinia(createPinia()) mockedActions = { - createTemporaryMessage: jest.fn() - .mockImplementation((context, { file, index, uploadId, localUrl, token }) => { - temporaryMessageCount += 1 - return { - id: temporaryMessageCount, - referenceId: 'reference-id-' + temporaryMessageCount, - token, - messageParameters: { - file: { - uploadId, - index, - token, - localUrl, - file, - }, - }, - } - }), addTemporaryMessage: jest.fn(), markTemporaryMessageAsFailed: jest.fn(), } @@ -78,6 +58,9 @@ describe('fileUploadStore', () => { storeConfig = cloneDeep(fileUploadStore) storeConfig.actions = Object.assign(storeConfig.actions, mockedActions) storeConfig.getters.getUserId = jest.fn().mockReturnValue(() => 'current-user') + storeConfig.getters.getActorId = jest.fn().mockReturnValue(() => 'current-user') + storeConfig.getters.getActorType = jest.fn().mockReturnValue(() => 'users') + storeConfig.getters.getDisplayName = jest.fn().mockReturnValue(() => 'Current User') }) afterEach(() => { @@ -103,7 +86,7 @@ describe('fileUploadStore', () => { restoreConsole() }) - test('initialises upload for given files', async () => { + test('initialises upload for given files', () => { const files = [ { name: 'pngimage.png', @@ -126,7 +109,7 @@ describe('fileUploadStore', () => { ] const localUrls = ['local-url:pngimage.png', 'local-url:jpgimage.jpg', undefined] - await store.dispatch('initialiseUpload', { + store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', files, @@ -135,10 +118,16 @@ describe('fileUploadStore', () => { const uploads = store.getters.getInitialisedUploads('upload-id1') expect(uploads).toHaveLength(files.length) - for (const index in files) { - expect(mockedActions.createTemporaryMessage.mock.calls[index][1]).toMatchObject({ - text: '{file}', + for (const index in uploads) { + expect(uploads[index][1].temporaryMessage).toMatchObject({ + message: '{file}', token: 'XXTOKENXX', + }) + expect(uploads[index][1].temporaryMessage.messageParameters.file).toMatchObject({ + type: 'file', + mimetype: files[index].type, + id: uploads[index][1].temporaryMessage.id, + name: files[index].name, uploadId: 'upload-id1', index: expect.anything(), file: files[index], @@ -155,7 +144,7 @@ describe('fileUploadStore', () => { lastModified: Date.UTC(2021, 3, 27, 15, 30, 0), } - await store.dispatch('initialiseUpload', { + store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', files: [file], @@ -164,6 +153,7 @@ describe('fileUploadStore', () => { expect(store.getters.currentUploadId).toBe('upload-id1') const uniqueFileName = '/Talk/' + file.name + 'uniq' + const referenceId = store.getters.getUploadsArray('upload-id1')[0][1].temporaryMessage.referenceId findUniquePath.mockResolvedValueOnce({ uniquePath: uniqueFileName, suffix: 1 }) uploadMock.mockResolvedValue() shareFile.mockResolvedValue() @@ -177,7 +167,7 @@ describe('fileUploadStore', () => { expect(uploadMock).toHaveBeenCalledWith(uniqueFileName, file) expect(shareFile).toHaveBeenCalledTimes(1) - expect(shareFile).toHaveBeenCalledWith(uniqueFileName, 'XXTOKENXX', 'reference-id-1', '{"caption":"text-caption","silent":true}') + expect(shareFile).toHaveBeenCalledWith(uniqueFileName, 'XXTOKENXX', referenceId, '{"caption":"text-caption","silent":true}') expect(mockedActions.addTemporaryMessage).toHaveBeenCalledTimes(1) expect(store.getters.currentUploadId).not.toBeDefined() @@ -198,7 +188,7 @@ describe('fileUploadStore', () => { } const files = [file1, file2] - await store.dispatch('initialiseUpload', { + store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', files, @@ -221,10 +211,11 @@ describe('fileUploadStore', () => { expect(findUniquePath).toHaveBeenNthCalledWith(+index + 1, client, '/files/current-user', '/Talk/' + files[index].name, undefined) expect(uploadMock).toHaveBeenNthCalledWith(+index + 1, `/Talk/${files[index].name}uniq`, files[index]) } + const referenceIds = store.getters.getUploadsArray('upload-id1').map(entry => entry[1].temporaryMessage.referenceId) 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(shareFile).toHaveBeenNthCalledWith(1, '/Talk/' + files[0].name + 'uniq', 'XXTOKENXX', referenceIds[0], '{}') + expect(shareFile).toHaveBeenNthCalledWith(2, '/Talk/' + files[1].name + 'uniq', 'XXTOKENXX', referenceIds[1], '{"caption":"text-caption"}') expect(mockedActions.addTemporaryMessage).toHaveBeenCalledTimes(2) expect(store.getters.currentUploadId).not.toBeDefined() @@ -240,7 +231,7 @@ describe('fileUploadStore', () => { }, ] - await store.dispatch('initialiseUpload', { + store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', files, @@ -262,7 +253,7 @@ describe('fileUploadStore', () => { expect(mockedActions.markTemporaryMessageAsFailed).toHaveBeenCalledTimes(1) expect(mockedActions.markTemporaryMessageAsFailed).toHaveBeenCalledWith(expect.anything(), { token: 'XXTOKENXX', - id: 1, + id: store.getters.getUploadsArray('upload-id1')[0][1].temporaryMessage.id, uploadId: 'upload-id1', reason: 'failed-upload' }) @@ -280,7 +271,7 @@ describe('fileUploadStore', () => { }, ] - await store.dispatch('initialiseUpload', { + store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', files, @@ -303,7 +294,7 @@ describe('fileUploadStore', () => { expect(mockedActions.markTemporaryMessageAsFailed).toHaveBeenCalledTimes(1) expect(mockedActions.markTemporaryMessageAsFailed).toHaveBeenCalledWith(expect.anything(), { token: 'XXTOKENXX', - id: 1, + id: store.getters.getUploadsArray('upload-id1')[0][1].temporaryMessage.id, uploadId: 'upload-id1', reason: 'failed-share' }) @@ -327,14 +318,14 @@ describe('fileUploadStore', () => { }, ] - await store.dispatch('initialiseUpload', { + store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', files, }) - // temporary message mock uses incremental id - await store.dispatch('removeFileFromSelection', 2) + const fileIds = store.getters.getUploadsArray('upload-id1').map(entry => entry[1].temporaryMessage.id) + await store.dispatch('removeFileFromSelection', fileIds[1]) const uploads = store.getters.getInitialisedUploads('upload-id1') expect(uploads).toHaveLength(1) @@ -358,7 +349,7 @@ describe('fileUploadStore', () => { }, ] - await store.dispatch('initialiseUpload', { + store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', files, @@ -372,7 +363,7 @@ describe('fileUploadStore', () => { expect(store.getters.currentUploadId).not.toBeDefined() }) - test('autorenames files using timestamps when requested', async () => { + test('autorenames files using timestamps when requested', () => { const files = [ { name: 'pngimage.png', @@ -388,7 +379,7 @@ describe('fileUploadStore', () => { }, ] - await store.dispatch('initialiseUpload', { + store.dispatch('initialiseUpload', { uploadId: 'upload-id1', token: 'XXTOKENXX', files, diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js index 3727e01c67b..885d7627edf 100644 --- a/src/store/messagesStore.js +++ b/src/store/messagesStore.js @@ -2,8 +2,6 @@ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import Hex from 'crypto-js/enc-hex.js' -import SHA256 from 'crypto-js/sha256.js' import cloneDeep from 'lodash/cloneDeep.js' import Vue from 'vue' @@ -29,7 +27,6 @@ import { postRichObjectToConversation, } from '../services/messagesService.ts' import { useCallViewStore } from '../stores/callView.js' -import { useChatExtrasStore } from '../stores/chatExtras.js' import { useGuestNameStore } from '../stores/guestName.js' import { usePollsStore } from '../stores/polls.ts' import { useReactionsStore } from '../stores/reactions.js' @@ -674,63 +671,6 @@ const actions = { } }, - /** - * Creates a temporary message ready to be posted, based - * on the message to be replied and the current actor - * - * @param {object} context default store context; - * @param {object} data the wrapping object; - * @param {string} data.text message string; - * @param {string} data.token conversation token; - * @param {string} data.uploadId upload id; - * @param {number} data.index index; - * @param {object} data.file file to upload; - * @param {string} data.localUrl local URL of file to upload; - * @param {boolean} data.isVoiceMessage whether the temporary file is a voice message - * @return {object} temporary message - */ - createTemporaryMessage(context, { text, token, uploadId, index, file, localUrl, isVoiceMessage }) { - const chatExtrasStore = useChatExtrasStore() - const parentId = chatExtrasStore.getParentIdToReply(token) - const parent = parentId && context.getters.message(token, parentId) - const date = new Date() - let tempId = 'temp-' + date.getTime() - const messageParameters = {} - if (file) { - tempId += '-' + uploadId + '-' + Math.random() - messageParameters.file = { - type: 'file', - file, - mimetype: file.type, - id: tempId, - name: file.newName || file.name, - // index, will be the id from now on - uploadId, - localUrl, - index, - } - } - - return Object.assign({}, { - id: tempId, - actorId: context.getters.getActorId(), - actorType: context.getters.getActorType(), - actorDisplayName: context.getters.getDisplayName(), - timestamp: 0, - systemMessage: '', - markdown: hasTalkFeature(token, 'markdown-messages'), - messageType: isVoiceMessage ? 'voice-message' : '', - message: text, - messageParameters, - token, - parent, - isReplyable: false, - sendingFailure: '', - reactions: {}, - referenceId: Hex.stringify(SHA256(tempId)), - }) - }, - /** * Add a temporary message generated in the client to * the store, these messages are deleted once the full diff --git a/src/store/messagesStore.spec.js b/src/store/messagesStore.spec.js index 105efce446c..ac841b3e528 100644 --- a/src/store/messagesStore.spec.js +++ b/src/store/messagesStore.spec.js @@ -29,7 +29,6 @@ import { postNewMessage, postRichObjectToConversation, } from '../services/messagesService.ts' -import { useChatExtrasStore } from '../stores/chatExtras.js' import { useGuestNameStore } from '../stores/guestName.js' import { useReactionsStore } from '../stores/reactions.js' import { generateOCSErrorResponse, generateOCSResponse } from '../test-helpers.js' @@ -517,193 +516,39 @@ describe('messagesStore', () => { describe('temporary messages', () => { let mockDate - let chatExtraStore beforeEach(() => { mockDate = new Date('2020-01-01 20:00:00') jest.spyOn(global, 'Date') .mockImplementation(() => mockDate) - chatExtraStore = useChatExtrasStore() }) - test('creates temporary message', async () => { - const temporaryMessage = await store.dispatch('createTemporaryMessage', { - text: 'blah', - token: TOKEN, - uploadId: null, - index: null, - file: null, - localUrl: null, - }) - - expect(getActorIdMock).toHaveBeenCalled() - expect(getActorTypeMock).toHaveBeenCalled() - expect(getDisplayNameMock).toHaveBeenCalled() - - expect(temporaryMessage).toMatchObject({ - id: 'temp-1577908800000', - actorId: 'actor-id-1', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - actorDisplayName: 'actor-display-name-1', - timestamp: 0, - systemMessage: '', - messageType: '', - message: 'blah', - messageParameters: {}, - token: TOKEN, - isReplyable: false, - sendingFailure: '', - reactions: {}, - referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), - }) - }) - - test('creates temporary message with message to be replied', async () => { - const parent = { - id: 123, - token: TOKEN, - message: 'hello', - } - - store.dispatch('processMessage', { token: TOKEN, message: parent }) - chatExtraStore.setParentIdToReply({ token: TOKEN, id: 123 }) - - const temporaryMessage = await store.dispatch('createTemporaryMessage', { - text: 'blah', + test('adds temporary message to the list', () => { + const temporaryMessage = { token: TOKEN, - uploadId: null, - index: null, - file: null, - localUrl: null, - }) - - expect(temporaryMessage).toMatchObject({ id: 'temp-1577908800000', - actorId: 'actor-id-1', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - actorDisplayName: 'actor-display-name-1', timestamp: 0, systemMessage: '', - messageType: '', - message: 'blah', - messageParameters: {}, - token: TOKEN, - parent, - isReplyable: false, - sendingFailure: '', - reactions: {}, - referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), - }) - }) - - test('creates temporary message with file', async () => { - const file = { - type: 'text/plain', - name: 'original-name.txt', - newName: 'new-name.txt', + messageType: 'comment', + message: 'original', } - const temporaryMessage = await store.dispatch('createTemporaryMessage', { - text: 'blah', - token: TOKEN, - uploadId: 'upload-id-1', - index: 'upload-index-1', - file, - localUrl: 'local-url://original-name.txt', - }) - - expect(temporaryMessage).toMatchObject({ - id: expect.stringMatching(/^temp-1577908800000-upload-id-1-0\.[0-9]*$/), - actorId: 'actor-id-1', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - actorDisplayName: 'actor-display-name-1', - timestamp: 0, - systemMessage: '', - messageType: '', - message: 'blah', - messageParameters: { - file: { - type: 'file', - file, - mimetype: 'text/plain', - id: expect.stringMatching(/^temp-1577908800000-upload-id-1-0\.[0-9]*$/), - name: 'new-name.txt', - uploadId: 'upload-id-1', - localUrl: 'local-url://original-name.txt', - index: 'upload-index-1', - }, - }, - token: TOKEN, - isReplyable: false, - sendingFailure: '', - reactions: {}, - referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), - }) - }) - - test('adds temporary message to the list', async () => { - const temporaryMessage = await store.dispatch('createTemporaryMessage', { - text: 'blah', - token: TOKEN, - uploadId: null, - index: null, - file: null, - localUrl: null, - }) store.dispatch('addTemporaryMessage', { token: TOKEN, message: temporaryMessage }) - expect(store.getters.messagesList(TOKEN)).toMatchObject([{ - id: 'temp-1577908800000', - actorId: 'actor-id-1', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - actorDisplayName: 'actor-display-name-1', - timestamp: 0, - systemMessage: '', - messageType: '', - message: 'blah', - messageParameters: {}, - token: TOKEN, - isReplyable: false, - sendingFailure: '', - reactions: {}, - referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), - }]) + expect(store.getters.messagesList(TOKEN)).toMatchObject([temporaryMessage]) expect(updateConversationLastActiveAction).toHaveBeenCalledWith(expect.anything(), TOKEN) + }) - // add again just replaces it - store.dispatch('addTemporaryMessage', { + test('marks temporary message as failed', () => { + const temporaryMessage = { token: TOKEN, - message: { ...temporaryMessage, message: 'replaced' } - }) - - expect(store.getters.messagesList(TOKEN)).toMatchObject([{ id: 'temp-1577908800000', - actorId: 'actor-id-1', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - actorDisplayName: 'actor-display-name-1', timestamp: 0, systemMessage: '', - messageType: '', - message: 'replaced', - messageParameters: {}, - token: TOKEN, - isReplyable: false, - sendingFailure: '', - reactions: {}, - referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), - }]) - }) - - test('marks temporary message as failed', async () => { - const temporaryMessage = await store.dispatch('createTemporaryMessage', { - text: 'blah', - token: TOKEN, - uploadId: null, - index: null, - file: null, - localUrl: null, - }) + messageType: 'comment', + message: 'original', + } store.dispatch('addTemporaryMessage', { token: TOKEN, message: temporaryMessage }) store.dispatch('markTemporaryMessageAsFailed', { @@ -713,67 +558,41 @@ describe('messagesStore', () => { }) expect(store.getters.messagesList(TOKEN)).toMatchObject([{ - id: 'temp-1577908800000', - actorId: 'actor-id-1', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - actorDisplayName: 'actor-display-name-1', - timestamp: 0, - systemMessage: '', - messageType: '', - message: 'blah', - messageParameters: {}, - token: TOKEN, - isReplyable: false, + ...temporaryMessage, sendingFailure: 'failure-reason', - reactions: {}, - referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), }]) }) - test('removeTemporaryMessageFromStore', async () => { - const temporaryMessage = await store.dispatch('createTemporaryMessage', { - text: 'blah', + test('removeTemporaryMessageFromStore', () => { + const temporaryMessage = { token: TOKEN, - uploadId: null, - index: null, - file: null, - localUrl: null, - }) + id: 'temp-1577908800000', + timestamp: 0, + systemMessage: '', + messageType: 'comment', + message: 'original', + } store.dispatch('addTemporaryMessage', { token: TOKEN, message: temporaryMessage }) - store.dispatch('removeTemporaryMessageFromStore', { token: TOKEN, id: temporaryMessage.id }) + store.dispatch('removeTemporaryMessageFromStore', { token: TOKEN, id: 'temp-1577908800000' }) expect(store.getters.messagesList(TOKEN)).toStrictEqual([]) }) - test('gets temporary message by reference', async () => { - const temporaryMessage = await store.dispatch('createTemporaryMessage', { - text: 'blah', + test('gets temporary message by reference', () => { + const temporaryMessage = { token: TOKEN, - uploadId: null, - index: null, - file: null, - localUrl: null, - }) - - store.dispatch('addTemporaryMessage', { token: TOKEN, message: temporaryMessage }) - - expect(store.getters.getTemporaryReferences(TOKEN, temporaryMessage.referenceId)).toMatchObject([{ id: 'temp-1577908800000', - actorId: 'actor-id-1', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - actorDisplayName: 'actor-display-name-1', timestamp: 0, systemMessage: '', - messageType: '', - message: 'blah', - messageParameters: {}, - token: TOKEN, - isReplyable: false, - sendingFailure: '', - reactions: {}, - referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), - }]) + messageType: 'comment', + message: 'original', + referenceId: 'reference-1', + } + + store.dispatch('addTemporaryMessage', { token: TOKEN, message: temporaryMessage }) + + expect(store.getters.getTemporaryReferences(TOKEN, 'reference-1')).toMatchObject([temporaryMessage]) }) }) diff --git a/src/utils/__tests__/prepareTemporaryMessage.spec.js b/src/utils/__tests__/prepareTemporaryMessage.spec.js new file mode 100644 index 00000000000..2458672a784 --- /dev/null +++ b/src/utils/__tests__/prepareTemporaryMessage.spec.js @@ -0,0 +1,124 @@ +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { ATTENDEE } from '../../constants.js' +import { prepareTemporaryMessage } from '../prepareTemporaryMessage.ts' + +describe('prepareTemporaryMessage', () => { + const TOKEN = 'XXTOKENXX' + let mockDate + + beforeEach(() => { + mockDate = new Date('2020-01-01 20:00:00') + jest.spyOn(global, 'Date') + .mockImplementation(() => mockDate) + }) + + const defaultPayload = { + message: 'message text', + token: TOKEN, + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'Actor One', + } + const defaultResult = { + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'Actor One', + expirationTimestamp: 0, + id: 'temp-1577908800000', + isReplyable: false, + markdown: true, + message: 'message text', + messageParameters: {}, + messageType: 'comment', + parent: undefined, + reactions: {}, + referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), + systemMessage: '', + timestamp: 0, + token: TOKEN, + } + + const parent = { + id: 123, + token: TOKEN, + message: 'hello', + } + + const textFile = { + type: 'text/plain', + name: 'original-name.txt', + newName: 'new-name.txt', + } + const textFilePayload = { + ...defaultPayload, + message: '{file}', + uploadId: 'upload-id-1', + index: 'upload-index-1', + file: textFile, + localUrl: 'local-url://original-name.txt', + } + const textFileResult = { + ...defaultResult, + id: expect.stringMatching(/^temp-1577908800000-upload-id-1-0\.[0-9]*$/), + message: '{file}', + messageParameters: { + file: { + type: 'file', + file: textFile, + mimetype: 'text/plain', + id: expect.stringMatching(/^temp-1577908800000-upload-id-1-0\.[0-9]*$/), + name: 'new-name.txt', + uploadId: 'upload-id-1', + localUrl: 'local-url://original-name.txt', + index: 'upload-index-1', + }, + }, + } + + const audioFile = { + type: 'audio/wav', + name: 'Talk recording from 2020-01-01 20-00-00.wav', + } + const audioFilePayload = { + ...defaultPayload, + message: '{file}', + messageType: 'voice-message', + uploadId: 'upload-id-1', + index: 'upload-index-1', + file: audioFile, + localUrl: 'local-url://original-name.txt', + } + const audioFileResult = { + ...defaultResult, + id: expect.stringMatching(/^temp-1577908800000-upload-id-1-0\.[0-9]*$/), + message: '{file}', + messageType: 'voice-message', + messageParameters: { + file: { + type: 'file', + file: audioFile, + mimetype: 'audio/wav', + id: expect.stringMatching(/^temp-1577908800000-upload-id-1-0\.[0-9]*$/), + name: 'Talk recording from 2020-01-01 20-00-00.wav', + uploadId: 'upload-id-1', + localUrl: 'local-url://original-name.txt', + index: 'upload-index-1', + }, + }, + } + + const tests = [ + [defaultPayload, defaultResult], + [{ ...defaultPayload, parent }, { ...defaultResult, parent }], + [textFilePayload, textFileResult], + [audioFilePayload, audioFileResult], + ] + + it.only.each(tests)('test case %# to match expected result', (payload, result) => { + const temporaryMessage = prepareTemporaryMessage(payload) + expect(temporaryMessage).toStrictEqual(result) + }) +}) diff --git a/src/utils/prepareTemporaryMessage.ts b/src/utils/prepareTemporaryMessage.ts new file mode 100644 index 00000000000..cee20246354 --- /dev/null +++ b/src/utils/prepareTemporaryMessage.ts @@ -0,0 +1,95 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import Hex from 'crypto-js/enc-hex.js' +import SHA256 from 'crypto-js/sha256.js' + +import { hasTalkFeature } from '../services/CapabilitiesManager.ts' +import type { ChatMessage, File } from '../types/index.ts' + +export type PrepareTemporaryMessagePayload = Pick & { + uploadId: string, + index: number, + file: File & { newName?: string }, + localUrl: string, + messageType?: 'voice-message' | 'comment', + parent: Omit, +} + +/** + * Creates a temporary message ready to be posted, based + * on the message to be replied and the current actor + * + * @param payload the wrapping object; + * @param payload.message message string; + * @param payload.token conversation token; + * @param payload.uploadId upload id; + * @param payload.index index; + * @param payload.file file to upload; + * @param payload.localUrl local URL of file to upload; + * @param payload.messageType specify when the temporary file is a voice message + * @param payload.actorId actor id + * @param payload.actorType actor type + * @param payload.actorDisplayName actor displayed name + * @param [payload.parent] parent message + */ +export function prepareTemporaryMessage({ + message, + token, + uploadId, + index, + file, + localUrl, + messageType = 'comment', + actorId, + actorType, + actorDisplayName, + parent, +}: PrepareTemporaryMessagePayload): ChatMessage { + const date = new Date() + let tempId = 'temp-' + date.getTime() + const messageParameters: ChatMessage['messageParameters'] = {} + if (file) { + tempId += '-' + uploadId + '-' + Math.random() + messageParameters.file = { + type: 'file', + // @ts-expect-error: 'file' does not exist in type RichObjectParameter + file, + mimetype: file.type, + id: tempId, + name: file.newName || file.name, + // index, will be the id from now on + uploadId, + localUrl, + index, + } + } + + return { + // @ts-expect-error: type 'string' is not assignable to type 'number' + id: tempId, + token, + timestamp: 0, + expirationTimestamp: 0, + systemMessage: '', + markdown: hasTalkFeature(token, 'markdown-messages'), + messageType, + message, + messageParameters, + parent, + isReplyable: false, + reactions: {}, + referenceId: Hex.stringify(SHA256(tempId)), + actorId, + actorType, + actorDisplayName, + } +}