Server IP : 80.87.202.40 / Your IP : 216.73.216.169 Web Server : Apache System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64 User : bitrix ( 600) PHP Version : 8.2.27 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/bitrix/ext_www/cvetdv.ru/bitrix/js/im/v2/provider/service/uploading/src/ |
Upload File : |
import { Type } from 'main.core'; import { BaseEvent, EventEmitter } from 'main.core.events'; import { getFilesFromDataTransfer, isFilePasted } from 'ui.uploader.core'; import { runAction } from 'im.v2.lib.rest'; import { Core } from 'im.v2.application.core'; import { FileStatus, FileType, RestMethod } from 'im.v2.const'; import { Utils } from 'im.v2.lib.utils'; import { Logger } from 'im.v2.lib.logger'; import { Notifier } from 'im.v2.lib.notifier'; import { SendingService } from 'im.v2.provider.service.sending'; import { UploaderWrapper } from './classes/uploader-wrapper'; import type { ImModelChat, ImModelUser, ImModelMessage, ImModelFile } from 'im.v2.model'; import type { UploaderFile, UploaderError } from 'ui.uploader.core'; import type { Store } from 'ui.vue3.vuex'; import type { RestClient } from 'rest.client'; import type { MessageWithFile, FileFromDisk, FileCommitParams, UploadFilesParams, UploadFromClipboardParams, UploadFromInputParams, UploadFromDragAndDrop, } from './types/uploading'; type CreateUploaderParams = { dialogId: number, autoUpload: boolean, maxParallelUploads: number, maxParallelLoads: number, }; const EVENT_NAMESPACE = 'BX.Messenger.v2.Service.UploadingService'; export class UploadingService extends EventEmitter { #store: Store; #restClient: RestClient; #isRequestingDiskFolderId: boolean = false; #diskFolderIdRequestPromise: { [string]: Promise } = {}; #uploaderWrapper: UploaderWrapper; #sendingService: SendingService; #uploaderFilesRegistry: { [uploaderId: string]: { autoUpload: boolean, wasSent: boolean, text: string, dialogId: string, tempMessageId: string, previewCreatedStatus: { [string]: boolean }, previewSentStatus: { [string]: boolean }, } } = {}; static event = { uploadStart: 'uploadStart', uploadComplete: 'uploadComplete', uploadError: 'uploadError', uploadCancel: 'uploadCancel', }; #queue: Array<string> = []; #isUploading: boolean = false; static instance = null; static getInstance(): UploadingService { if (!this.instance) { this.instance = new this(); } return this.instance; } constructor() { super(); this.setEventNamespace(EVENT_NAMESPACE); this.#store = Core.getStore(); this.#restClient = Core.getRestClient(); this.#sendingService = SendingService.getInstance(); this.#initUploader(); } async uploadFromClipboard(params: UploadFromClipboardParams): Promise<{files: UploaderFile[], uploaderId: string}> { const { clipboardEvent, dialogId, autoUpload, imagesOnly } = params; const { clipboardData } = clipboardEvent; if (!clipboardData || !isFilePasted(clipboardData)) { return ''; } clipboardEvent.preventDefault(); let files: File[] = await getFilesFromDataTransfer(clipboardData); if (imagesOnly) { files = files.filter((file) => Utils.file.isImage(file.name)); if (imagesOnly.length === 0) { return ''; } } const { uploaderFiles, uploaderId } = await this.addFiles({ files, dialogId, autoUpload, }); if (uploaderFiles.length === 0) { return ''; } return uploaderId; } async uploadFromInput(params: UploadFromInputParams): Promise<string> { const { event, sendAsFile, autoUpload, dialogId } = params; const rawFiles = Object.values(event.target.files); if (rawFiles.length === 0) { return ''; } const { uploaderId } = await this.addFiles({ files: rawFiles, dialogId, autoUpload, sendAsFile, }); return uploaderId; } async uploadFromDragAndDrop(params: UploadFromDragAndDrop): Promise<string> { const { event, dialogId, autoUpload, sendAsFile } = params; event.preventDefault(); const rawFiles = await getFilesFromDataTransfer(event.dataTransfer); if (rawFiles.length === 0) { return ''; } const { uploaderId } = await this.addFiles({ files: rawFiles, dialogId, autoUpload, sendAsFile, }); return uploaderId; } #createUploader(params: CreateUploaderParams): Promise<string> { const { dialogId, autoUpload, maxParallelUploads, maxParallelLoads, } = params; const uploaderId = Utils.text.getUuidV4(); const chatId = this.#getChatId(dialogId); return this.checkDiskFolderId(dialogId).then((diskFolderId: number) => { this.#uploaderWrapper.createUploader({ diskFolderId, uploaderId, autoUpload, chatId, dialogId, maxParallelUploads, maxParallelLoads, }); return uploaderId; }); } addFiles(params: UploadFilesParams): Promise<{uploaderFiles: UploaderFile[], uploaderId: string}> { const { files, dialogId, autoUpload, sendAsFile = false, maxParallelUploads, maxParallelLoads, } = params; const uploaderParams: CreateUploaderParams = { dialogId, autoUpload, maxParallelUploads, maxParallelLoads, }; return this.#createUploader(uploaderParams).then((uploaderId: string) => { const filesForUploader = []; files.forEach((file) => { const preparedFile = this.#prepareFileForUploader(file, dialogId, uploaderId, sendAsFile); filesForUploader.push(preparedFile); }); const addedFiles = this.#uploaderWrapper.addFiles(filesForUploader); this.#registerFiles(uploaderId, addedFiles, dialogId, autoUpload); return { uploaderFiles: addedFiles, uploaderId, }; }); } getFiles(uploaderId): UploaderFile[] { return this.#uploaderWrapper.getFiles(uploaderId); } #addToQueue(uploaderId: string) { this.#queue.push(uploaderId); this.#processQueue(); } #processQueue() { if (this.#isUploading || this.#queue.length === 0) { return; } this.#isUploading = true; const uploaderId: string = this.#queue.shift(); this.#uploaderFilesRegistry[uploaderId].autoUpload = true; this.#uploaderWrapper.start(uploaderId); } start(uploaderId: string) { this.#addToQueue(uploaderId); } stop(uploaderId: string) { this.#uploaderWrapper.stop(uploaderId); } uploadFileFromDisk(files, dialogId) { Object.values(files).forEach((file) => { const messageWithFile = this.#prepareFileFromDisk(file, dialogId); this.#addFileFromDiskToModel(messageWithFile).then(() => { const message = { tempMessageId: messageWithFile.tempMessageId, fileIds: [messageWithFile.tempFileId], dialogId: messageWithFile.dialogId, }; return this.#sendingService.sendMessageWithFiles(message); }).then(() => { this.commitFile({ chatId: messageWithFile.chatId, temporaryFileId: messageWithFile.tempFileId, tempMessageId: messageWithFile.tempMessageId, realFileId: messageWithFile.file.id.slice(1), fromDisk: true, }); }).catch((error) => { console.error('SendingService: sendFilesFromDisk error:', error); }); }); } #addFileFromDiskToModel(messageWithFile: MessageWithFile): Promise { return this.#store.dispatch('files/add', { id: messageWithFile.tempFileId, chatId: messageWithFile.chatId, authorId: Core.getUserId(), name: messageWithFile.file.name, type: Utils.file.getFileTypeByExtension(messageWithFile.file.ext), extension: messageWithFile.file.ext, size: messageWithFile.file.sizeInt, status: FileStatus.wait, progress: 0, authorName: this.#getCurrentUser().name, }); } #initUploader() { this.#uploaderWrapper = new UploaderWrapper(); this.#uploaderWrapper.subscribe(UploaderWrapper.events.onFileAddStart, (event: BaseEvent) => { const { file } = event.getData(); this.#addFileToStore(file); }); this.#uploaderWrapper.subscribe(UploaderWrapper.events.onFileAdd, (event: BaseEvent) => { const { file, uploaderId } = event.getData(); this.#updateFilePreviewInStore(file); this.#setPreviewCreatedStatus(uploaderId, file.getId()); this.#tryToSendMessage(uploaderId); }); this.#uploaderWrapper.subscribe(UploaderWrapper.events.onFileUploadStart, (event: BaseEvent) => { const { file } = event.getData(); this.#updateFileSizeInStore(file); this.emit(UploadingService.event.uploadStart); }); this.#uploaderWrapper.subscribe(UploaderWrapper.events.onFileUploadProgress, (event: BaseEvent) => { const { file } = event.getData(); this.#updateFileProgress(file.getId(), file.getProgress(), FileStatus.upload); }); this.#uploaderWrapper.subscribe(UploaderWrapper.events.onFileUploadComplete, async (event: BaseEvent) => { const { file, uploaderId }: {file: UploaderFile, uploaderId: string} = event.getData(); const serverFileId: number = file.getServerFileId().toString().slice(1); const temporaryFileId: string = file.getId(); if (this.#isMediaFile(temporaryFileId)) { this.#setFileMapping({ serverFileId, temporaryFileId }); } this.#updateFileProgress(temporaryFileId, file.getProgress(), FileStatus.wait); await this.#uploadPreview(file); this.#setPreviewSentStatus(uploaderId, temporaryFileId); void this.#tryCommit(uploaderId); this.emit(UploadingService.event.uploadComplete); }); this.#uploaderWrapper.subscribe(UploaderWrapper.events.onFileUploadError, (event: BaseEvent) => { const { file, error, uploaderId } = event.getData(); this.#setMessageError(file.getCustomData('tempMessageId')); this.getFiles(uploaderId).forEach((uploaderFile: UploaderFile) => { this.#updateFileProgress(uploaderFile.getId(), 0, FileStatus.error); }); if (this.#isMaxFileSizeExceeded(error)) { Notifier.file.handleUploadError(error); } Logger.error('UploadingService: upload error', error); this.emit(UploadingService.event.uploadError); this.#isUploading = false; this.#processQueue(); this.stop(uploaderId); }); this.#uploaderWrapper.subscribe(UploaderWrapper.events.onFileUploadCancel, (event: BaseEvent) => { const { tempMessageId, tempFileId } = event.getData(); this.#cancelUpload(tempMessageId, tempFileId); this.emit(UploadingService.event.uploadCancel); }); this.#uploaderWrapper.subscribe(UploaderWrapper.events.onUploadComplete, () => { this.#isUploading = false; this.#processQueue(); }); } #isMediaFile(fileId: string | number): boolean { const mediaFileTypes: Array<string> = [FileType.image, FileType.video]; const file: ?ImModelFile = this.#store.getters['files/get'](fileId); return Boolean(file) && mediaFileTypes.includes(file.type); } #setFileMapping(options: {serverFileId: number, temporaryFileId: string}) { void this.#store.dispatch('files/setTemporaryFileMapping', options); } checkDiskFolderId(dialogId: string): Promise<number> { if (this.#getDiskFolderId(dialogId) > 0) { return Promise.resolve(this.#getDiskFolderId(dialogId)); } if (this.#isRequestingDiskFolderId) { return this.#diskFolderIdRequestPromise[dialogId]; } this.#diskFolderIdRequestPromise[dialogId] = this.#requestDiskFolderId(dialogId); return this.#diskFolderIdRequestPromise[dialogId]; } #requestDiskFolderId(dialogId: string): Promise { return new Promise((resolve, reject) => { this.#isRequestingDiskFolderId = true; const chatId = this.#getChatId(dialogId); this.#restClient.callMethod(RestMethod.imDiskFolderGet, { chat_id: chatId }).then((response) => { const { ID: diskFolderId } = response.data(); this.#isRequestingDiskFolderId = false; void this.#store.dispatch('chats/update', { dialogId, fields: { diskFolderId, }, }); resolve(diskFolderId); }).catch((error) => { this.#isRequestingDiskFolderId = false; reject(error); }); }); } commitFile(params: FileCommitParams) { const { temporaryFileId, tempMessageId, chatId, realFileId, fromDisk, messageText = '', sendAsFile = false } = params; const fileIdParams = {}; if (fromDisk) { fileIdParams.disk_id = realFileId; } else { fileIdParams.upload_id = realFileId.toString().slice(1); } this.#restClient.callMethod(RestMethod.imDiskFileCommit, { chat_id: chatId, message: messageText, template_id: tempMessageId, file_template_id: temporaryFileId, as_file: sendAsFile ? 'Y' : 'N', ...fileIdParams, }).catch((error) => { this.#setMessageError(tempMessageId); this.#updateFileProgress(temporaryFileId, 0, FileStatus.error); console.error('commitFile error', error); }); } commitMessage(uploaderId: string): Promise { const dialogId = this.#uploaderFilesRegistry[uploaderId].dialogId; const chatId = this.#getChatId(dialogId); const fileIds = this.#uploaderWrapper.getFiles(uploaderId).map((file) => { return file.getServerFileId().toString().slice(1); }); const sendAsFile = this.#uploaderWrapper.getFiles(uploaderId).every((file) => { return file.getCustomData('sendAsFile'); }); return this.#restClient.callMethod(RestMethod.imDiskFileCommit, { chat_id: chatId, message: this.#uploaderFilesRegistry[uploaderId].text, template_id: this.#uploaderFilesRegistry[uploaderId].tempMessageId, // file_template_id: temporaryFileId, as_file: sendAsFile ? 'Y' : 'N', upload_id: fileIds, }); } async #tryCommit(uploaderId: string) { if (!this.#readyToCommit(uploaderId)) { return; } await this.commitMessage(uploaderId); this.#destroyUploader(uploaderId); } async #uploadPreview(file: UploaderFile): Promise { const needPreview = this.#getFileType(file.getBinary()) === FileType.video || file.isAnimated(); if (!needPreview) { return Promise.resolve(); } const id = file.getServerFileId().toString().slice(1); const previewFile = file.getClientPreview(); if (!previewFile) { file.setCustomData('sendAsFile', true); return Promise.resolve(); } const formData = new FormData(); formData.append('id', id); formData.append('previewFile', previewFile, `preview_${file.getName()}.jpg`); return runAction(RestMethod.imDiskFilePreviewUpload, { data: formData }) .catch(([error]) => { console.error('imDiskFilePreviewUpload request error', error); }); } #prepareFileForUploader(file: File, dialogId: string, uploaderId, sendAsFile: boolean): MessageWithFile { const tempMessageId = Utils.text.getUuidV4(); const tempFileId = Utils.text.getUuidV4(); const chatId = this.#getChatId(dialogId); return { tempMessageId, tempFileId, file, dialogId, chatId, uploaderId, sendAsFile, }; } #updateFileProgress(id: string, progress: number, status: string) { void this.#store.dispatch('files/update', { id, fields: { progress: (progress === 100 ? 99 : progress), status, }, }); } #cancelUpload(tempMessageId: string, tempFileId) { const message: ImModelMessage = this.#store.getters['messages/getById'](tempMessageId); if (message) { void this.#store.dispatch('messages/delete', { id: tempMessageId }); void this.#store.dispatch('files/delete', { id: tempFileId }); const chat: ImModelChat = this.#store.getters['chats/getByChatId'](message.chatId); const lastMessageId: string | number | null = this.#store.getters['messages/findLastChatMessageId'](message.chatId); if (Type.isString(lastMessageId) || Type.isNumber(lastMessageId)) { void this.#store.dispatch('recent/update', { id: chat.dialogId, fields: { messageId: lastMessageId }, }); } else { void this.#store.dispatch('recent/delete', { id: chat.dialogId, }); } } } #addFileToStore(file: UploaderFile) { const taskId = file.getId(); const fileBinary = file.getBinary(); const previewData = this.#preparePreview(file); const asFile = file.getCustomData('sendAsFile'); void this.#store.dispatch('files/add', { id: taskId, chatId: file.getCustomData('chatId'), authorId: Core.getUserId(), name: fileBinary.name, size: file.getSize(), type: asFile ? FileType.file : this.#getFileType(fileBinary), extension: this.#getFileExtension(fileBinary), status: file.isFailed() ? FileStatus.error : FileStatus.progress, progress: 0, authorName: this.#getCurrentUser().name, urlDownload: URL.createObjectURL(file.getBinary()), ...previewData, }); } #updateFilePreviewInStore(file: UploaderFile) { const previewData = this.#preparePreview(file); void this.#store.dispatch('files/update', { id: file.getId(), fields: { ...previewData, }, }); } #updateFileSizeInStore(file: UploaderFile) { void this.#store.dispatch('files/update', { id: file.getId(), fields: { size: file.getSize(), }, }); } #preparePreview(file: UploaderFile): { image: { width: number, height: number }, urlPreview: Blob } { if (file.getCustomData('sendAsFile')) { return {}; } const preview = { blob: file.getPreviewUrl(), width: file.getPreviewWidth(), height: file.getPreviewHeight(), }; const previewData = {}; if (preview.blob) { previewData.image = { width: preview.width, height: preview.height, }; previewData.urlPreview = preview.blob; } if (file.getClientPreview()) { previewData.urlPreview = URL.createObjectURL(file.getClientPreview()); } return previewData; } #getDiskFolderId(dialogId: string): number { return this.#getDialog(dialogId).diskFolderId; } #getFileType(file: File): string { let fileType = FileType.file; if (file.type.startsWith('image')) { fileType = FileType.image; } else if (file.type.startsWith('video')) { fileType = FileType.video; } return fileType; } #getFileExtension(file: File): string { return file.name.split('.').splice(-1)[0]; } #getDialog(dialogId: string): ImModelChat { return this.#store.getters['chats/get'](dialogId); } #getCurrentUser(): ImModelUser { const userId = Core.getUserId(); return this.#store.getters['users/get'](userId); } #getChatId(dialogId: string): ?number { return this.#getDialog(dialogId)?.chatId; } #registerFiles(uploaderId: string, files: UploaderFile[], dialogId: string, autoUpload: boolean) { if (!this.#uploaderFilesRegistry[uploaderId]) { this.#uploaderFilesRegistry[uploaderId] = { previewCreatedStatus: {}, previewSentStatus: {}, dialogId, text: '', autoUpload, }; } files.forEach((file) => { const fileId = file.getId(); if (!this.#uploaderFilesRegistry[uploaderId].previewCreatedStatus) { this.#uploaderFilesRegistry[uploaderId].previewCreatedStatus = {}; } this.#uploaderFilesRegistry[uploaderId].previewCreatedStatus[fileId] = false; this.#uploaderFilesRegistry[uploaderId].previewSentStatus[fileId] = false; }); } #unregisterFiles(uploaderId: string, files: UploaderFile[]) { const entry = this.#uploaderFilesRegistry[uploaderId]; if (entry) { files.forEach((file: UploaderFile) => { const fileId = file.getId(); if (this.#uploaderFilesRegistry[uploaderId].previewCreatedStatus) { delete this.#uploaderFilesRegistry[uploaderId].previewCreatedStatus[fileId]; delete this.#uploaderFilesRegistry[uploaderId].previewSentStatus[fileId]; } }); } } #setPreviewCreatedStatus(uploaderId: string, fileId: string) { this.#uploaderFilesRegistry[uploaderId].previewCreatedStatus[fileId] = true; } #setPreviewSentStatus(uploaderId: string, fileId: string) { this.#uploaderFilesRegistry[uploaderId].previewSentStatus[fileId] = true; } #setMessagesText(uploaderId: string, text: string) { this.#uploaderFilesRegistry[uploaderId].text = text; } #setAutoUpload(uploaderId: string, autoUploadFlag: boolean) { this.#uploaderFilesRegistry[uploaderId].autoUpload = autoUploadFlag; } // we don't use it now, because we always send several files in ONE message // noinspection JSUnusedGlobalSymbols sendSeparateMessagesWithFiles(params: { uploaderId: string, text: string}) { const { uploaderId, text } = params; this.#setMessagesText(uploaderId, text); this.#setAutoUpload(uploaderId, true); this.#tryToSendMessages(uploaderId); } sendMessageWithFiles(params: { uploaderId: string, text: string}) { const { uploaderId, text } = params; this.#setMessagesText(uploaderId, text); this.#setAutoUpload(uploaderId, true); this.#tryToSendMessage(uploaderId); } #createMessagesFromFiles(uploaderId): {comment: {text: string, dialogId: string}, files: []} { const messagesToSend = { comment: {}, files: [], }; const files = this.getFiles(uploaderId); const text = this.#uploaderFilesRegistry[uploaderId].text; const dialogId = this.#uploaderFilesRegistry[uploaderId].dialogId; const hasText = text.length > 0; // if we have more than one file and text, we need to send text message first if (files.length > 1 && hasText) { messagesToSend.comment = { dialogId, text }; } files.forEach((file) => { if (file.getError()) { return; } const messageId = Utils.text.getUuidV4(); file.setCustomData('messageId', messageId); if (files.length === 1 && hasText) { file.setCustomData('messageText', text); } messagesToSend.files.push({ fileIds: [file.getId()], tempMessageId: file.getCustomData('tempMessageId'), dialogId, text: file.getCustomData('messageText') ?? '', }); }); return messagesToSend; } #createMessageFromFiles(uploaderId): {text: string, dialogId: string, tempMessageId: string, fileIds: []} { const fileIds = []; const files = this.getFiles(uploaderId); files.forEach((file) => { if (!file.getError()) { fileIds.push(file.getId()); } }); const text = this.#uploaderFilesRegistry[uploaderId].text; const dialogId = this.#uploaderFilesRegistry[uploaderId].dialogId; const tempMessageId = files[0].getCustomData('tempMessageId'); this.#uploaderFilesRegistry[uploaderId].tempMessageId = tempMessageId; return { fileIds, tempMessageId, dialogId, text, }; } #readyToAddMessages(uploaderId): boolean { if ( !this.#uploaderFilesRegistry[uploaderId] || !this.#uploaderFilesRegistry[uploaderId].autoUpload || this.#uploaderFilesRegistry[uploaderId].wasSent ) { return false; } const { previewCreatedStatus } = this.#uploaderFilesRegistry[uploaderId]; return Object.values(previewCreatedStatus).every((flag) => flag === true); } #readyToCommit(uploaderId: string): boolean { if (!this.#uploaderFilesRegistry[uploaderId]) { return false; } const { previewSentStatus } = this.#uploaderFilesRegistry[uploaderId]; return Object.values(previewSentStatus).every((flag) => flag === true); } #tryToSendMessage(uploaderId: string) { if (!this.#readyToAddMessages(uploaderId)) { return; } this.#uploaderFilesRegistry[uploaderId].wasSent = true; const message = this.#createMessageFromFiles(uploaderId); void this.#sendingService.sendMessageWithFiles(message); this.start(uploaderId); } // we don't use it now, because we always send several files in ONE message // noinspection JSUnusedGlobalSymbols #tryToSendMessages(uploaderId: string) { if (!this.#readyToAddMessages(uploaderId)) { return; } this.#uploaderFilesRegistry[uploaderId].wasSent = true; const { comment, files } = this.#createMessagesFromFiles(uploaderId); if (comment.text) { this.#sendingService.sendMessage(comment); } files.forEach((message) => { void this.#sendingService.sendMessageWithFiles(message); }); this.start(uploaderId); } #prepareFileFromDisk(file: FileFromDisk, dialogId: string): MessageWithFile { const tempMessageId = Utils.text.getUuidV4(); const realFileId = file.id.slice(1); // 'n123' => '123' const tempFileId = `${tempMessageId}|${realFileId}`; return { tempMessageId, tempFileId, dialogId, file, chatId: this.#getDialog(dialogId).chatId, }; } #isMaxFileSizeExceeded(error: UploaderError): boolean { return error.getCode() === 'MAX_FILE_SIZE_EXCEEDED'; } #setMessageError(tempMessageId: string) { void this.#store.dispatch('messages/update', { id: tempMessageId, fields: { error: true, }, }); } getUploaderIdByFileId(fileId: string | number): ?string { const uploaderIds: Array<string> = Object.keys(this.#uploaderFilesRegistry); return uploaderIds.find((uploaderId: string) => { return this.getFiles(uploaderId).some((file: UploaderFile) => { return file.getId() === fileId; }); }); } removeFileFromUploader(options: { uploaderId: string, filesIds: Array<string>, restartUploading: boolean }) { const { uploaderId, filesIds, restartUploading = false } = options; const files: Array<UploaderFile> = this.#uploaderWrapper.getFiles(uploaderId).filter((file: UploaderFile) => { return filesIds.includes(file.getId()); }); files.forEach((file: UploaderFile) => { file.remove(); file.abort(); }); this.#unregisterFiles(uploaderId, files); if (restartUploading) { const [firstFile] = this.getFiles(uploaderId); if (firstFile) { firstFile.upload(); } } } destroy() { this.#uploaderWrapper.destroy(); } #destroyUploader(uploaderId: string) { this.#uploaderWrapper.destroyUploader(uploaderId); delete this.#uploaderFilesRegistry[uploaderId]; } #getBinaryFiles(uploaderId: string): Array<File> { return this.getFiles(uploaderId).map((file: UploaderFile) => { return file.getBinary(); }); } async retry(uploaderId: string) { const { dialogId, text, tempMessageId } = this.#uploaderFilesRegistry[uploaderId]; const sendAsFile: boolean = this.getFiles(uploaderId).some((file) => { return file.getCustomData('sendAsFile'); }); const binaryFiles: Array<File> = this.#getBinaryFiles(uploaderId); const { uploaderId: newUploaderId } = await this.addFiles({ dialogId, files: binaryFiles, sendAsFile, }); void this.#store.dispatch('messages/delete', { id: tempMessageId, }); this.sendMessageWithFiles({ uploaderId: newUploaderId, text, }); this.#destroyUploader(uploaderId); } }