'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 } from '@/lib/chat-media'; import { cn } from '@/lib/utils'; export interface ChatMediaComposeItem { id: string; file: File; previewUrl: string | null; 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), 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; return ( !next && onClose()}>
{items.length > 1 ? `Отправить ${items.length} файла` : 'Отправить изображение'}
{activePreview ? (
{activePreview.previewUrl ? ( // eslint-disable-next-line @next/next/no-img-element {activePreview.file.name} ) : (

{activePreview.file.name}

{formatFileSize(activePreview.file.size)}

)}
) : null} {items.length > 1 ? (
{items.map((item) => ( ))}
) : activePreview?.previewUrl ? (

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

) : 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 DropZone({ title, subtitle, className, onDrop }: { title: string; subtitle: string; className?: string; onDrop: (files: File[]) => void; }) { return (
{ event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; }} onDrop={(event) => { event.preventDefault(); event.stopPropagation(); const files = Array.from(event.dataTransfer.files ?? []); if (files.length) onDrop(files); }} >

{title}

{subtitle}

); } export function extractFilesFromDataTransfer(dataTransfer: DataTransfer | null) { if (!dataTransfer) return [] as File[]; return Array.from(dataTransfer.files ?? []); } export function extractFilesFromClipboard(dataTransfer: DataTransfer | null) { if (!dataTransfer) return [] as File[]; const files: File[] = []; for (const item of Array.from(dataTransfer.items ?? [])) { if (item.kind !== 'file') continue; const file = item.getAsFile(); if (file) files.push(file); } if (files.length) return files; return Array.from(dataTransfer.files ?? []); }