This commit is contained in:
lendry
2026-06-26 17:49:22 +03:00
parent c23f35e732
commit 4e853f8041
17 changed files with 1547 additions and 167 deletions

View File

@@ -0,0 +1,121 @@
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<string>;
export async function uploadChatMediaBatch(params: {
roomId: string;
token: string;
items: ChatMediaUploadItem[];
caption?: string;
isE2E: boolean;
userId?: string;
peerE2eKey?: string | null;
encryptOutgoing?: EncryptOutgoing;
}): Promise<ChatMessage[]> {
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 === '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<PresignedUploadResponse>(
`/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;
}