import { apiFetch, PresignedUploadResponse, sendChatMessage, uploadMediaObject, type ChatMessage } from '@/lib/api'; import { compressChatImage, isCompressibleImageFile, readImageDimensions } from '@/lib/chat-image-compress'; import { detectChatMessageType, resolveChatContentType } from '@/lib/chat-media'; import { e2eKindFromMessageType, encryptE2EBlob, type E2EMessagePayload } from '@/lib/e2e-chat'; export interface ChatMediaUploadItem { file: File; sendAsFile: boolean; width?: number; height?: number; } type EncryptOutgoing = (payload: E2EMessagePayload) => Promise; export async function uploadChatMediaBatch(params: { roomId: string; token: string; items: ChatMediaUploadItem[]; caption?: string; isE2E: boolean; userId?: string; peerE2eKey?: string | null; encryptOutgoing?: EncryptOutgoing; }): Promise { const { roomId, token, items, caption, isE2E, userId, peerE2eKey, encryptOutgoing } = params; if (!items.length) return []; const albumId = crypto.randomUUID(); const created: ChatMessage[] = []; for (let index = 0; index < items.length; index += 1) { const item = items[index]!; let file = item.file; let width = item.width; let height = item.height; const shouldCompress = isCompressibleImageFile(file) && !item.sendAsFile; if (shouldCompress) { const compressed = await compressChatImage(file); file = compressed.file; width = compressed.width ?? width; height = compressed.height ?? height; } else if (isCompressibleImageFile(file) && (!width || !height)) { const dimensions = await readImageDimensions(file); width = dimensions.width; height = dimensions.height; } const contentType = resolveChatContentType(file); const messageType = item.sendAsFile && isCompressibleImageFile(item.file) ? 'FILE' : detectChatMessageType(file); const metadata = { fileName: item.file.name, fileSize: file.size, albumId, albumIndex: index, albumCount: items.length, ...(width ? { width } : {}), ...(height ? { height } : {}), ...(item.sendAsFile ? { sendAsFile: true } : {}) }; let uploadFile = file; let uploadContentType = contentType; let messageContent: string | undefined = index === 0 && caption?.trim() ? caption.trim() : messageType === 'VIDEO' ? 'Видео' : messageType === 'VIDEO_NOTE' ? 'Видеосообщение' : messageType === 'FILE' ? item.file.name : undefined; let isEncrypted = false; if (isE2E) { if (!userId || !peerE2eKey || !encryptOutgoing) { throw new Error('Секретный чат недоступен без ключей E2E у обоих участников'); } const encryptedBuffer = await encryptE2EBlob(userId, peerE2eKey, await file.arrayBuffer()); uploadFile = new File([encryptedBuffer], `${item.file.name}.lendryenc`, { type: 'application/octet-stream' }); uploadContentType = 'application/octet-stream'; messageContent = await encryptOutgoing({ kind: e2eKindFromMessageType(messageType), fileName: item.file.name, mimeType: contentType, fileSize: file.size, text: index === 0 ? caption?.trim() : undefined }); isEncrypted = true; } const presigned = await apiFetch( `/media/chat/${roomId}/media/upload-url`, { method: 'POST', body: JSON.stringify({ contentType: uploadContentType, fileName: uploadFile.name }) }, token ); await uploadMediaObject(presigned, uploadFile, uploadContentType); const message = await sendChatMessage( roomId, { type: messageType, storageKey: presigned.storageKey, mimeType: uploadContentType, content: messageContent, isEncrypted, metadataJson: JSON.stringify(isE2E ? { e2e: true, ...metadata } : metadata) }, token ); created.push(message); } return created; }