Skip to content

Commit 9fc9d06

Browse files
committed
upload shared files in parallel, pass caption to server with the last file
Signed-off-by: Maksim Sukharev <[email protected]>
1 parent 3615c73 commit 9fc9d06

File tree

3 files changed

+116
-93
lines changed

3 files changed

+116
-93
lines changed

src/components/NewMessage/NewMessageUploadEditor.vue

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040
tag="div"
4141
group>
4242
<template v-for="file in files">
43-
<FilePreview :key="file.temporaryMessage.id"
43+
<FilePreview :key="file[1].temporaryMessage.id"
4444
:token="token"
45-
v-bind="file.temporaryMessage.messageParameters.file"
45+
v-bind="file[1].temporaryMessage.messageParameters.file"
4646
:is-upload-editor="true"
4747
@remove-file="handleRemoveFileFromSelection" />
4848
</template>
@@ -117,10 +117,7 @@ export default {
117117
},
118118
119119
files() {
120-
if (this.currentUploadId) {
121-
return this.$store.getters.getInitialisedUploads(this.currentUploadId)
122-
}
123-
return []
120+
return this.$store.getters.getInitialisedUploads(this.currentUploadId)
124121
},
125122
126123
showModal() {
@@ -136,7 +133,7 @@ export default {
136133
},
137134
138135
firstFile() {
139-
return this.files[Object.keys(this.files)[0]]
136+
return this.files?.at(0)?.at(1)
140137
},
141138
142139
// Hide the plus button in case this editor is used while sending a voice message

src/store/fileUploadStore.js

Lines changed: 45 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -48,36 +48,24 @@ const state = {
4848

4949
const getters = {
5050

51-
getInitialisedUploads: (state) => (uploadId) => {
51+
getUploadsArray: (state) => (uploadId) => {
5252
if (state.uploads[uploadId]) {
53-
const initialisedUploads = {}
54-
for (const index in state.uploads[uploadId].files) {
55-
const currentFile = state.uploads[uploadId].files[index]
56-
if (currentFile.status === 'initialised') {
57-
initialisedUploads[index] = (currentFile)
58-
}
59-
}
60-
return initialisedUploads
53+
return Object.entries(state.uploads[uploadId].files)
6154
} else {
62-
return {}
55+
return []
6356
}
6457
},
6558

59+
getInitialisedUploads: (state, getters) => (uploadId) => {
60+
return getters.getUploadsArray(uploadId)
61+
.filter(([_index, uploadedFile]) => uploadedFile.status === 'initialised')
62+
},
63+
6664
// Returns all the files that have been successfully uploaded provided an
6765
// upload id
68-
getShareableFiles: (state) => (uploadId) => {
69-
if (state.uploads[uploadId]) {
70-
const shareableFiles = {}
71-
for (const index in state.uploads[uploadId].files) {
72-
const currentFile = state.uploads[uploadId].files[index]
73-
if (currentFile.status === 'successUpload') {
74-
shareableFiles[index] = (currentFile)
75-
}
76-
}
77-
return shareableFiles
78-
} else {
79-
return {}
80-
}
66+
getShareableFiles: (state, getters) => (uploadId) => {
67+
return getters.getUploadsArray(uploadId)
68+
.filter(([_index, uploadedFile]) => uploadedFile.status === 'successUpload')
8169
},
8270

8371
// gets the current attachment folder
@@ -293,22 +281,27 @@ const actions = {
293281

294282
EventBus.$emit('upload-start')
295283

296-
// Tag the previously indexed files and add the temporary messages to the
297-
// messages list
298-
for (const index in state.uploads[uploadId].files) {
284+
// Tag previously indexed files and add temporary messages to the MessagesList
285+
// If caption is provided, attach to the last temporary message
286+
const lastIndex = getters.getUploadsArray(uploadId).at(-1).at(0)
287+
for (const [index, uploadedFile] of getters.getUploadsArray(uploadId)) {
299288
// mark all files as uploading
300289
commit('markFileAsUploading', { uploadId, index })
301290
// Store the previously created temporary message
302-
const temporaryMessage = state.uploads[uploadId].files[index].temporaryMessage
291+
const temporaryMessage = {
292+
...uploadedFile.temporaryMessage,
293+
message: index === lastIndex ? caption : '{file}',
294+
}
303295
// Add temporary messages (files) to the messages list
304296
dispatch('addTemporaryMessage', temporaryMessage)
305297
// Scroll the message list
306298
EventBus.$emit('scroll-chat-to-bottom')
307299
}
300+
308301
// Iterate again and perform the uploads
309-
for (const index in state.uploads[uploadId].files) {
302+
await Promise.allSettled(getters.getUploadsArray(uploadId).map(async ([index, uploadedFile]) => {
310303
// currentFile to be uploaded
311-
const currentFile = state.uploads[uploadId].files[index].file
304+
const currentFile = uploadedFile.file
312305
// userRoot path
313306
const userRoot = '/files/' + getters.getUserId()
314307
const fileName = (currentFile.newName || currentFile.name)
@@ -345,41 +338,34 @@ const actions = {
345338
showError(t('spreed', 'Error while uploading file "{fileName}"', { fileName }))
346339
}
347340

348-
const temporaryMessage = state.uploads[uploadId].files[index].temporaryMessage
349341
// Mark the upload as failed in the store
350342
commit('markFileAsFailedUpload', { uploadId, index })
351-
dispatch('markTemporaryMessageAsFailed', {
352-
message: temporaryMessage,
353-
reason,
354-
})
343+
dispatch('markTemporaryMessageAsFailed', { message: uploadedFile.temporaryMessage, reason })
355344
}
356-
357-
// Get the files that have successfully been uploaded from the store
358-
const shareableFiles = getters.getShareableFiles(uploadId)
359-
// Share each of those files to the conversation
360-
for (const index in shareableFiles) {
361-
const path = shareableFiles[index].sharePath
362-
const temporaryMessage = shareableFiles[index].temporaryMessage
363-
const metadata = JSON.stringify({ messageType: temporaryMessage.messageType })
364-
try {
365-
const token = temporaryMessage.token
366-
dispatch('markFileAsSharing', { uploadId, index })
367-
await shareFile(path, token, temporaryMessage.referenceId, metadata)
368-
dispatch('markFileAsShared', { uploadId, index })
369-
} catch (error) {
370-
if (error?.response?.status === 403) {
371-
showError(t('spreed', 'You are not allowed to share files'))
372-
} else {
373-
showError(t('spreed', 'An error happened when trying to share your file'))
374-
}
375-
dispatch('markTemporaryMessageAsFailed', {
376-
message: temporaryMessage,
377-
reason: 'failed-share',
378-
})
379-
console.error('An error happened when trying to share your file: ', error)
345+
}))
346+
347+
// Share the files, that have successfully been uploaded from the store, to the conversation
348+
await Promise.all(getters.getShareableFiles(uploadId).map(async ([index, shareableFile]) => {
349+
const path = shareableFile.sharePath
350+
const temporaryMessage = shareableFile.temporaryMessage
351+
const metadata = (caption && index === lastIndex)
352+
? JSON.stringify({ messageType: temporaryMessage.messageType, caption })
353+
: JSON.stringify({ messageType: temporaryMessage.messageType })
354+
try {
355+
const token = temporaryMessage.token
356+
dispatch('markFileAsSharing', { uploadId, index })
357+
await shareFile(path, token, temporaryMessage.referenceId, metadata)
358+
dispatch('markFileAsShared', { uploadId, index })
359+
} catch (error) {
360+
if (error?.response?.status === 403) {
361+
showError(t('spreed', 'You are not allowed to share files'))
362+
} else {
363+
showError(t('spreed', 'An error happened when trying to share your file'))
380364
}
365+
dispatch('markTemporaryMessageAsFailed', { message: temporaryMessage, reason: 'failed-share' })
366+
console.error('An error happened when trying to share your file: ', error)
381367
}
382-
}
368+
}))
383369
EventBus.$emit('upload-finished')
384370
},
385371
/**

src/store/fileUploadStore.spec.js

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -111,29 +111,67 @@ describe('fileUploadStore', () => {
111111
lastModified: Date.UTC(2021, 3, 25, 15, 30, 0),
112112
},
113113
]
114+
const localUrls = ['local-url:pngimage.png', 'local-url:jpgimage.jpg', 'icon-url:text/plain']
115+
114116
await store.dispatch('initialiseUpload', {
115117
uploadId: 'upload-id1',
116118
token: 'XXTOKENXX',
117119
files,
118120
})
119121

120122
const uploads = store.getters.getInitialisedUploads('upload-id1')
121-
expect(Object.keys(uploads).length).toBe(3)
122-
123-
for (let i = 0; i < files.length; i++) {
124-
expect(mockedActions.createTemporaryMessage.mock.calls[i][1].text).toBe('{file}')
125-
expect(mockedActions.createTemporaryMessage.mock.calls[i][1].uploadId).toBe('upload-id1')
126-
expect(mockedActions.createTemporaryMessage.mock.calls[i][1].index).toBeDefined()
127-
expect(mockedActions.createTemporaryMessage.mock.calls[i][1].file).toBe(files[i])
128-
expect(mockedActions.createTemporaryMessage.mock.calls[i][1].token).toBe('XXTOKENXX')
123+
expect(uploads).toHaveLength(files.length)
124+
125+
for (const index in files) {
126+
expect(mockedActions.createTemporaryMessage.mock.calls[index][1]).toMatchObject({
127+
text: '{file}',
128+
token: 'XXTOKENXX',
129+
uploadId: 'upload-id1',
130+
index: expect.anything(),
131+
file: files[index],
132+
localUrl: localUrls[index],
133+
})
134+
}
135+
})
136+
137+
test('performs upload and sharing of single file', async () => {
138+
const file = {
139+
name: 'pngimage.png',
140+
type: 'image/png',
141+
size: 123,
142+
lastModified: Date.UTC(2021, 3, 27, 15, 30, 0),
129143
}
144+
const fileBuffer = await new Blob([file]).arrayBuffer()
145+
146+
await store.dispatch('initialiseUpload', {
147+
uploadId: 'upload-id1',
148+
token: 'XXTOKENXX',
149+
files: [file],
150+
})
151+
152+
expect(store.getters.currentUploadId).toBe('upload-id1')
130153

131-
expect(mockedActions.createTemporaryMessage.mock.calls[0][1].localUrl).toBe('local-url:pngimage.png')
132-
expect(mockedActions.createTemporaryMessage.mock.calls[1][1].localUrl).toBe('local-url:jpgimage.jpg')
133-
expect(mockedActions.createTemporaryMessage.mock.calls[2][1].localUrl).toBe('icon-url:text/plain')
154+
const uniqueFileName = '/Talk/' + file.name + 'uniq'
155+
findUniquePath.mockResolvedValueOnce(uniqueFileName)
156+
client.putFileContents.mockResolvedValue()
157+
shareFile.mockResolvedValue()
158+
159+
await store.dispatch('uploadFiles', { uploadId: 'upload-id1', caption: 'text-caption' })
160+
161+
expect(findUniquePath).toHaveBeenCalledTimes(1)
162+
expect(findUniquePath).toHaveBeenCalledWith(client, '/files/current-user', '/Talk/' + file.name)
163+
164+
expect(client.putFileContents).toHaveBeenCalledTimes(1)
165+
expect(client.putFileContents).toHaveBeenCalledWith(`/files/current-user${uniqueFileName}`, fileBuffer, expect.anything())
166+
167+
expect(shareFile).toHaveBeenCalledTimes(1)
168+
expect(shareFile).toHaveBeenCalledWith(`/${uniqueFileName}`, 'XXTOKENXX', 'reference-id-1', '{"caption":"text-caption"}')
169+
170+
expect(mockedActions.addTemporaryMessage).toHaveBeenCalledTimes(1)
171+
expect(store.getters.currentUploadId).not.toBeDefined()
134172
})
135173

136-
test('performs upload by uploading then sharing', async () => {
174+
test('performs upload and sharing of multiple files', async () => {
137175
const file1 = {
138176
name: 'pngimage.png',
139177
type: 'image/png',
@@ -164,23 +202,25 @@ describe('fileUploadStore', () => {
164202
.mockResolvedValueOnce('/Talk/' + files[0].name + 'uniq')
165203
.mockResolvedValueOnce('/Talk/' + files[1].name + 'uniq')
166204
client.putFileContents.mockResolvedValue()
167-
shareFile.mockResolvedValue()
205+
shareFile
206+
.mockResolvedValueOnce({ data: { ocs: { data: { id: '1' } } } })
207+
.mockResolvedValueOnce({ data: { ocs: { data: { id: '2' } } } })
168208

169-
await store.dispatch('uploadFiles', 'upload-id1')
209+
await store.dispatch('uploadFiles', { uploadId: 'upload-id1', caption: 'text-caption' })
170210

211+
expect(findUniquePath).toHaveBeenCalledTimes(2)
171212
expect(client.putFileContents).toHaveBeenCalledTimes(2)
172213
expect(shareFile).toHaveBeenCalledTimes(2)
173214

174-
for (let i = 0; i < files.length; i++) {
175-
expect(findUniquePath).toHaveBeenCalledWith(client, '/files/current-user', '/Talk/' + files[i].name)
176-
expect(client.putFileContents.mock.calls[i][0]).toBe('/files/current-user/Talk/' + files[i].name + 'uniq')
177-
expect(client.putFileContents.mock.calls[i][1]).toStrictEqual(fileBuffers[i])
178-
179-
expect(shareFile.mock.calls[i][0]).toBe('//Talk/' + files[i].name + 'uniq')
180-
expect(shareFile.mock.calls[i][1]).toBe('XXTOKENXX')
181-
expect(shareFile.mock.calls[i][2]).toBe('reference-id-' + (i + 1))
215+
for (const index in files) {
216+
expect(findUniquePath).toHaveBeenCalledWith(client, '/files/current-user', '/Talk/' + files[index].name)
217+
expect(client.putFileContents).toHaveBeenCalledWith(`/files/current-user/Talk/${files[index].name}uniq`, fileBuffers[index], expect.anything())
182218
}
183219

220+
expect(shareFile).toHaveBeenCalledTimes(2)
221+
expect(shareFile).toHaveBeenNthCalledWith(1, '//Talk/' + files[0].name + 'uniq', 'XXTOKENXX', 'reference-id-1', '{}')
222+
expect(shareFile).toHaveBeenNthCalledWith(2, '//Talk/' + files[1].name + 'uniq', 'XXTOKENXX', 'reference-id-2', '{"caption":"text-caption"}')
223+
184224
expect(mockedActions.addTemporaryMessage).toHaveBeenCalledTimes(2)
185225
expect(store.getters.currentUploadId).not.toBeDefined()
186226
})
@@ -209,7 +249,7 @@ describe('fileUploadStore', () => {
209249
},
210250
})
211251

212-
await store.dispatch('uploadFiles', 'upload-id1')
252+
await store.dispatch('uploadFiles', { uploadId: 'upload-id1' })
213253

214254
expect(client.putFileContents).toHaveBeenCalledTimes(1)
215255
expect(shareFile).not.toHaveBeenCalled()
@@ -247,7 +287,7 @@ describe('fileUploadStore', () => {
247287
},
248288
})
249289

250-
await store.dispatch('uploadFiles', 'upload-id1')
290+
await store.dispatch('uploadFiles', { uploadId: 'upload-id1' })
251291

252292
expect(client.putFileContents).toHaveBeenCalledTimes(1)
253293
expect(shareFile).toHaveBeenCalledTimes(1)
@@ -286,9 +326,9 @@ describe('fileUploadStore', () => {
286326
await store.dispatch('removeFileFromSelection', 2)
287327

288328
const uploads = store.getters.getInitialisedUploads('upload-id1')
289-
expect(Object.keys(uploads).length).toBe(1)
329+
expect(uploads).toHaveLength(1)
290330

291-
expect(Object.values(uploads)[0].file).toBe(files[0])
331+
expect(uploads[0][1].file).toBe(files[0])
292332
})
293333

294334
test('discard an entire upload', async () => {
@@ -316,7 +356,7 @@ describe('fileUploadStore', () => {
316356
await store.dispatch('discardUpload', 'upload-id1')
317357

318358
const uploads = store.getters.getInitialisedUploads('upload-id1')
319-
expect(uploads).toStrictEqual({})
359+
expect(uploads).toStrictEqual([])
320360

321361
expect(store.getters.currentUploadId).not.toBeDefined()
322362
})

0 commit comments

Comments
 (0)