diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts index 2736bd9fbf8d5..9b5cf6da3d17b 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -58,7 +58,7 @@ const getAttachmentForFile = async (fileToUpload: EncryptedUpload): Promise undefined), }), }; }; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index d243389333a9a..e5e80380651cd 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -12,21 +12,102 @@ import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { i18n } from '../../../app/utils/lib/i18n'; import { settings } from '../settings'; +type PersistedUpload = { + id: string; + fileName: string; + fileType: string; + fileSize: number; + url: string; + encryption?: { + key: JsonWebKey; + iv: string; + type: string; + hash: string; + metadataForEncryption: Partial; + }; +}; + class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }> implements UploadsAPI { private rid: string; private tmid?: string; + private storageKey: string; + constructor({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }) { super(); this.rid = rid; this.tmid = tmid; + this.storageKey = `composer_uploads_${rid}${tmid ? `-${tmid}` : ''}`; + this.restoreFromStorage(); } private uploads: readonly Upload[] = []; private processingUploads: boolean = false; + private restoreFromStorage(): void { + try { + const stored = localStorage.getItem(this.storageKey); + if (!stored) { + return; + } + + const persisted: PersistedUpload[] = JSON.parse(stored); + this.uploads = persisted.map((p) => { + const file = new File([], p.fileName, { type: p.fileType }); + Object.defineProperty(file, 'size', { value: p.fileSize, configurable: true }); + + return { + id: p.id, + file, + url: p.url, + percentage: 100, + ...(p.encryption && { + encryptedFile: { + file: new File([], p.fileName, { type: p.encryption.type }), + key: p.encryption.key, + iv: p.encryption.iv, + type: p.encryption.type, + hash: p.encryption.hash, + }, + metadataForEncryption: p.encryption.metadataForEncryption, + }), + }; + }); + } catch { + localStorage.removeItem(this.storageKey); + } + } + + private persistToStorage(): void { + const completedUploads = this.uploads.filter((u) => u.url && !u.error); + + if (completedUploads.length === 0) { + localStorage.removeItem(this.storageKey); + return; + } + + const serialized: PersistedUpload[] = completedUploads.map((u) => ({ + id: u.id, + fileName: u.file.name, + fileType: u.file.type, + fileSize: u.file.size, + url: u.url!, + ...(isEncryptedUpload(u) && { + encryption: { + key: u.encryptedFile.key, + iv: u.encryptedFile.iv, + type: u.encryptedFile.type, + hash: u.encryptedFile.hash, + metadataForEncryption: u.metadataForEncryption, + }, + }), + })); + + localStorage.setItem(this.storageKey, JSON.stringify(serialized)); + } + set = (uploads: Upload[]): void => { this.uploads = uploads; this.emit('update'); @@ -57,6 +138,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' removeUpload = (id: Upload['id']): void => { this.set(this.uploads.filter((upload) => upload.id !== id)); + this.persistToStorage(); if (this.uploads.length === 0) { UserAction.stop(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid }); @@ -71,15 +153,22 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' return upload; } + const originalSize = upload.file.size; + const newFile = new File([upload.file], fileName, upload.file); + if (newFile.size !== originalSize) { + Object.defineProperty(newFile, 'size', { value: originalSize, configurable: true }); + } + return { ...upload, - file: new File([upload.file], fileName, upload.file), + file: newFile, ...(isEncryptedUpload(upload) && { metadataForEncryption: { ...upload.metadataForEncryption, name: fileName }, }), }; }), ); + this.persistToStorage(); } catch (error) { this.set( this.uploads.map((upload) => { @@ -176,6 +265,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' if (xhr.status === 200) { const result = JSON.parse(xhr.responseText); this.updateUpload(id, { id: result.file._id, url: result.file.url, percentage: 100 }); + this.persistToStorage(); return; }