Skip to content

Commit

Permalink
Merge pull request #13784 from nextcloud/fix/noid/temp-message-util
Browse files Browse the repository at this point in the history
  • Loading branch information
Antreesy authored Nov 17, 2024
2 parents aade00d + 3071d4d commit 71f9d6c
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 325 deletions.
11 changes: 7 additions & 4 deletions src/components/NewMessage/NewMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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(),
Expand All @@ -301,6 +303,7 @@ export default {
supportTypingStatus,
autoComplete,
userData,
createTemporaryMessage,
}
},

Expand Down Expand Up @@ -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 = ''
Expand Down Expand Up @@ -835,15 +838,15 @@ 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
}
// 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 })
},

/**
Expand Down
2 changes: 1 addition & 1 deletion src/components/NewMessage/NewMessageAudioRecorder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/NewMessage/NewMessageUploadEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
},

Expand Down
37 changes: 37 additions & 0 deletions src/composables/useTemporaryMessage.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
21 changes: 15 additions & 6 deletions src/store/fileUploadStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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]
Expand All @@ -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 })
}
},

Expand Down Expand Up @@ -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 } : {},
Expand Down
71 changes: 31 additions & 40 deletions src/store/fileUploadStore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand All @@ -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(() => {
Expand All @@ -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',
Expand All @@ -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,
Expand All @@ -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],
Expand All @@ -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],
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -198,7 +188,7 @@ describe('fileUploadStore', () => {
}
const files = [file1, file2]

await store.dispatch('initialiseUpload', {
store.dispatch('initialiseUpload', {
uploadId: 'upload-id1',
token: 'XXTOKENXX',
files,
Expand All @@ -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()
Expand All @@ -240,7 +231,7 @@ describe('fileUploadStore', () => {
},
]

await store.dispatch('initialiseUpload', {
store.dispatch('initialiseUpload', {
uploadId: 'upload-id1',
token: 'XXTOKENXX',
files,
Expand All @@ -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'
})
Expand All @@ -280,7 +271,7 @@ describe('fileUploadStore', () => {
},
]

await store.dispatch('initialiseUpload', {
store.dispatch('initialiseUpload', {
uploadId: 'upload-id1',
token: 'XXTOKENXX',
files,
Expand All @@ -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'
})
Expand All @@ -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)
Expand All @@ -358,7 +349,7 @@ describe('fileUploadStore', () => {
},
]

await store.dispatch('initialiseUpload', {
store.dispatch('initialiseUpload', {
uploadId: 'upload-id1',
token: 'XXTOKENXX',
files,
Expand All @@ -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',
Expand All @@ -388,7 +379,7 @@ describe('fileUploadStore', () => {
},
]

await store.dispatch('initialiseUpload', {
store.dispatch('initialiseUpload', {
uploadId: 'upload-id1',
token: 'XXTOKENXX',
files,
Expand Down
Loading

0 comments on commit 71f9d6c

Please sign in to comment.