'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Loader2, Plus, Smile, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { createFilePreviewUrl, isCompressibleImageFile, readImageDimensions } from '@/lib/chat-image-compress'; import { formatFileSize, getComposeDialogTitle, getComposeMediaKind, type ComposeMediaKind } from '@/lib/chat-media'; import { cn } from '@/lib/utils'; export interface ChatMediaComposeItem { id: string; file: File; previewUrl: string | null; mediaKind: ComposeMediaKind; sendAsFile: boolean; width?: number; height?: number; } const MAX_FILES = 10; function createComposeItem(file: File, sendAsFile = false): ChatMediaComposeItem { return { id: crypto.randomUUID(), file, previewUrl: createFilePreviewUrl(file), mediaKind: getComposeMediaKind(file), sendAsFile }; } export function useChatMediaComposer() { const [open, setOpen] = useState(false); const [items, setItems] = useState([]); const [caption, setCaption] = useState(''); const [sendAsFile, setSendAsFile] = useState(false); const reset = useCallback(() => { setItems((current) => { current.forEach((item) => { if (item.previewUrl) URL.revokeObjectURL(item.previewUrl); }); return []; }); setCaption(''); setSendAsFile(false); }, []); const close = useCallback(() => { setOpen(false); reset(); }, [reset]); const openWithFiles = useCallback(async (files: File[], options?: { sendAsFile?: boolean }) => { const limited = files.slice(0, MAX_FILES); const initialSendAsFile = options?.sendAsFile ?? false; const nextItems = await Promise.all( limited.map(async (file) => { const item = createComposeItem(file, initialSendAsFile); if (isCompressibleImageFile(file)) { const dimensions = await readImageDimensions(file); item.width = dimensions.width; item.height = dimensions.height; } return item; }) ); setItems(nextItems); setSendAsFile(initialSendAsFile); setCaption(''); setOpen(true); }, []); const addFiles = useCallback(async (files: File[]) => { const limited = files.slice(0, MAX_FILES); if (!limited.length) return; const appended = await Promise.all( limited.map(async (file) => { const item = createComposeItem(file, sendAsFile); if (isCompressibleImageFile(file)) { const dimensions = await readImageDimensions(file); item.width = dimensions.width; item.height = dimensions.height; } return item; }) ); setItems((current) => [...current, ...appended].slice(0, MAX_FILES)); }, [sendAsFile]); const removeItem = useCallback((id: string) => { setItems((current) => { const target = current.find((item) => item.id === id); if (target?.previewUrl) URL.revokeObjectURL(target.previewUrl); const next = current.filter((item) => item.id !== id); if (!next.length) { setOpen(false); setCaption(''); setSendAsFile(false); } return next; }); }, []); useEffect(() => { setItems((current) => current.map((item) => ({ ...item, sendAsFile }))); }, [sendAsFile]); const hasImages = useMemo(() => items.some((item) => isCompressibleImageFile(item.file)), [items]); return { open, items, caption, sendAsFile, hasImages, maxFiles: MAX_FILES, setCaption, setSendAsFile, openWithFiles, addFiles, removeItem, close }; } interface ChatMediaComposeDialogProps { open: boolean; items: ChatMediaComposeItem[]; caption: string; sendAsFile: boolean; hasImages: boolean; maxFiles: number; sending?: boolean; onCaptionChange: (value: string) => void; onSendAsFileChange: (value: boolean) => void; onRemoveItem: (id: string) => void; onAddFiles: (files: File[]) => void; onClose: () => void; onSend: () => void; } export function ChatMediaComposeDialog({ open, items, caption, sendAsFile, hasImages, maxFiles, sending = false, onCaptionChange, onSendAsFileChange, onRemoveItem, onAddFiles, onClose, onSend }: ChatMediaComposeDialogProps) { const addInputRef = useRef(null); const activePreview = items[0] ?? null; const dialogTitle = getComposeDialogTitle(items.map((item) => item.file)); return ( !next && onClose()}>
{dialogTitle}
{activePreview ? (
) : null} {items.length > 1 ? (
{items.map((item) => ( ))}
) : activePreview?.mediaKind === 'image' && activePreview.previewUrl ? (

Для редактирования нажмите на изображение

) : activePreview?.mediaKind === 'video' ? (

Проверьте видео перед отправкой

) : null} {hasImages ? ( ) : null}
onCaptionChange(event.target.value)} placeholder="Подпись" className="rounded-xl pr-10" onKeyDown={(event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); onSend(); } }} />
{ const files = Array.from(event.target.files ?? []); if (files.length) onAddFiles(files); event.target.value = ''; }} />
); } interface ChatMediaDropOverlayProps { visible: boolean; onDropCompressed: (files: File[]) => void; onDropAsFile: (files: File[]) => void; } export function ChatMediaDropOverlay({ visible, onDropCompressed, onDropAsFile }: ChatMediaDropOverlayProps) { if (!visible) return null; return (
); } function ComposeMediaPreview({ item, className, thumbnail = false }: { item: ChatMediaComposeItem; className?: string; thumbnail?: boolean; }) { if (item.mediaKind === 'video' && item.previewUrl) { return (