Skip to content

Commit 984ea0a

Browse files
committed
feat(upload): compress images eagerly
Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
1 parent b06f0dd commit 984ea0a

4 files changed

Lines changed: 59 additions & 6 deletions

File tree

src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,9 @@ export default {
255255
return ''
256256
}
257257
258-
const size = this.uploadFile.totalSize
258+
const size = this.uploadStore.skipCompression
259+
? this.uploadFile.totalSize
260+
: this.uploadStore.compressedFileSizes[this.referenceId] ?? this.uploadFile.totalSize
259261
return size ? formatFileSize(size, true) : ''
260262
},
261263

src/components/NewMessage/NewMessageUploadEditor.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
:token="token"
9595
isUploadEditor
9696
:file="file[1].temporaryMessage.messageParameters.file"
97+
:referenceId="file[1].temporaryMessage.referenceId"
9798
@removeFile="removeFile" />
9899
</TransitionWrapper>
99100
<div v-else class="upload-editor__voice-message">

src/stores/__tests__/upload.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,13 +608,14 @@ describe('fileUploadStore', () => {
608608
getTalkConfig.mockReturnValue(true)
609609
})
610610

611-
test('does not compress anything when not requested', async () => {
611+
test('starts eager compression on upload', async () => {
612612
const file = makeImage()
613+
compressImage.mockResolvedValue(makeCompressed())
613614
uploadStore.initialiseUpload({ uploadId: 'upload-id1', token: TOKEN, files: [file] })
614615

615616
await uploadStore.uploadFiles({ token: TOKEN, uploadId: 'upload-id1', options: null })
616617

617-
expect(compressImage).not.toHaveBeenCalled()
618+
expect(compressImage).toHaveBeenCalled()
618619
expect(uploadMock).toHaveBeenCalledWith(expect.anything(), file)
619620
})
620621

src/stores/upload.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export const useUploadStore = defineStore('upload', () => {
8585
const uploads = reactive<UploadsState>({})
8686
const currentUploadId = ref<string | undefined>(undefined)
8787
const localUrls = reactive<Record<string, string>>({})
88+
const compressionJobs: Record<string, Promise<File | null> | undefined> = {}
89+
const compressedFileSizes = reactive<Record<string, number>>({})
8890

8991
/**
9092
* The user's choices for the current upload. They are only valid until it
@@ -168,7 +170,7 @@ export const useUploadStore = defineStore('upload', () => {
168170
/**
169171
* Returns the local URL of uploaded image
170172
*
171-
* @param referenceId
173+
* @param referenceId the temporary message's reference id
172174
*/
173175
function getLocalUrl(referenceId: string): string | undefined {
174176
return localUrls[referenceId]
@@ -213,6 +215,9 @@ export const useUploadStore = defineStore('upload', () => {
213215
if (localUrl) {
214216
localUrls[temporaryMessage.referenceId] = localUrl
215217
}
218+
if (supportImageCompression(file.type) && file.size > 0) {
219+
void initCompressImage(temporaryMessage.referenceId, file)
220+
}
216221
}
217222

218223
/**
@@ -306,6 +311,7 @@ export const useUploadStore = defineStore('upload', () => {
306311
const uploadId = currentUploadId.value!
307312
for (const index in uploads[uploadId].files) {
308313
if (uploads[uploadId].files[index].temporaryMessage!.id === temporaryMessageId) {
314+
dismissCompressImage(uploads[uploadId].files[index].temporaryMessage!.referenceId)
309315
delete uploads[uploadId].files[index]
310316
}
311317
}
@@ -374,9 +380,50 @@ export const useUploadStore = defineStore('upload', () => {
374380
}
375381
EventBus.emit('upload-discard')
376382

383+
for (const [_index, uploadedFile] of getUploadsArray(uploadId)) {
384+
dismissCompressImage(uploadedFile.temporaryMessage.referenceId)
385+
}
386+
377387
delete uploads[uploadId]
378388
}
379389

390+
/**
391+
* Compresses a staged image eagerly, exposing resulting preview size.
392+
* The job output is kept, so compressUploadedImage() can reuse it.
393+
* Starts a job only once per file, and returns the pending or finished one.
394+
*
395+
* @param referenceId the temporary message's reference id
396+
* @param file the staged file
397+
*/
398+
function initCompressImage(referenceId: string, file: File): Promise<File | null> {
399+
const startedJob = compressionJobs[referenceId]
400+
if (startedJob) {
401+
return startedJob
402+
}
403+
const newJob = async () => {
404+
try {
405+
const compressed = await compressImage(file)
406+
compressedFileSizes[referenceId] = compressed?.size ?? file.size
407+
return compressed
408+
} catch (error) {
409+
console.error('Failed to compress image, uploading original: ', error)
410+
return null
411+
}
412+
}
413+
compressionJobs[referenceId] = newJob()
414+
return compressionJobs[referenceId]
415+
}
416+
417+
/**
418+
* Drops the job and size preview, release resources
419+
*
420+
* @param referenceId the temporary message's reference id
421+
*/
422+
function dismissCompressImage(referenceId: string) {
423+
delete compressionJobs[referenceId]
424+
delete compressedFileSizes[referenceId]
425+
}
426+
380427
/**
381428
* Re-encodes an initialised image upload in place, replacing the staged file
382429
* and its local preview URL.
@@ -395,9 +442,10 @@ export const useUploadStore = defineStore('upload', () => {
395442
return null
396443
}
397444

445+
const referenceId = uploadedFile.temporaryMessage.referenceId
398446
try {
399447
// @ts-expect-error: UploadFile.file is a custom type, not a File
400-
const compressed = await compressImage(currentFile)
448+
const compressed = await initCompressImage(referenceId, currentFile)
401449
if (!compressed) {
402450
// Compression was not beneficial, the original file is kept
403451
return null
@@ -407,7 +455,6 @@ export const useUploadStore = defineStore('upload', () => {
407455
uploads[uploadId].files[index].file = compressed
408456
uploads[uploadId].files[index].totalSize = compressed.size
409457

410-
const referenceId = uploadedFile.temporaryMessage.referenceId
411458
if (localUrls[referenceId]) {
412459
URL.revokeObjectURL(localUrls[referenceId])
413460
}
@@ -449,6 +496,7 @@ export const useUploadStore = defineStore('upload', () => {
449496
const compressed = compressImages
450497
? await compressUploadedImage(uploadId, index)
451498
: null
499+
dismissCompressImage(uploadedFile.temporaryMessage.referenceId)
452500

453501
// Store the previously created temporary message
454502
const message = {
@@ -823,6 +871,7 @@ export const useUploadStore = defineStore('upload', () => {
823871
uploads,
824872
currentUploadId,
825873
localUrls,
874+
compressedFileSizes,
826875
allowUpdate,
827876
skipCompression,
828877

0 commit comments

Comments
 (0)