import type { IMessage, IRoom, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; import { UserAction, USER_ACTIVITIES } from '../../../app/ui/client/lib/UserAction'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { getErrorMessage } from '../errorHandling'; import type { UploadsAPI } from './ChatAPI'; import type { Upload } from './Upload'; let uploads: readonly Upload[] = []; const emitter = new Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }>(); const updateUploads = (update: (uploads: readonly Upload[]) => readonly Upload[]): void => { uploads = update(uploads); emitter.emit('update'); }; const get = (): readonly Upload[] => uploads; const subscribe = (callback: () => void): (() => void) => emitter.on('update', callback); const cancel = (id: Upload['id']): void => { emitter.emit(`cancelling-${id}`); }; const wipeFailedOnes = (): void => { updateUploads((uploads) => uploads.filter((upload) => !upload.error)); }; const send = async ( file: File, { description, msg, rid, tmid, t, }: { description?: string; msg?: string; rid: string; tmid?: string; t?: IMessage['t']; }, getContent?: (fileId: string, fileUrl: string) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ): Promise => { const id = Random.id(); const upload: Upload = { id, name: fileContent?.raw.name || file.name, percentage: 0, }; updateUploads((uploads) => [...uploads, upload]); try { await new Promise((resolve, reject) => { const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, { file, ...(fileContent && { content: JSON.stringify(fileContent.encrypted), }), }, { load: (event) => { resolve(event); }, progress: (event) => { if (!event.lengthComputable) { return; } const progress = (event.loaded / event.total) * 100; if (progress === 100) { return; } updateUploads((uploads) => uploads.map((upload) => { if (upload.id !== id) { return upload; } return { ...upload, percentage: Math.round(progress) || 0, }; }), ); }, error: (event) => { updateUploads((uploads) => uploads.map((upload) => { if (upload.id !== id) { return upload; } return { ...upload, percentage: 0, error: new Error(xhr.responseText), }; }), ); reject(event); }, }, ); xhr.onload = async () => { if (xhr.readyState === xhr.DONE) { if (xhr.status === 400) { const error = JSON.parse(xhr.responseText); updateUploads((uploads) => [...uploads, { ...upload, error: new Error(error.error) }]); return; } if (xhr.status === 200) { const result = JSON.parse(xhr.responseText); let content; if (getContent) { content = await getContent(result.file._id, result.file.url); } await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { msg, tmid, description, t, content, }); } } }; if (uploads.length) { UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); } emitter.once(`cancelling-${id}`, () => { xhr.abort(); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); }); }); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error: unknown) { updateUploads((uploads) => uploads.map((upload) => { if (upload.id !== id) { return upload; } return { ...upload, percentage: 0, error: new Error(getErrorMessage(error)), }; }), ); } finally { if (!uploads.length) { UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); } } }; export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => ({ get, subscribe, wipeFailedOnes, cancel, send: ( file: File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string, fileUrl: string) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), });