import { useCallback, useEffect, useRef, useState } from 'react'; import { AlertCircle, X } from 'lucide-react'; import { AttachmentPreviewItem } from './AttachmentPreviewItem'; import { ImageLightbox } from './ImageLightbox'; import type { AttachmentPayload } from '@shared/types'; const ANIMATION_MS = 400; interface AttachmentPreviewListProps { attachments: AttachmentPayload[]; onRemove: (id: string) => void; error?: string | null; onDismissError?: () => void; /** When true, previews are overlaid with a disabled indicator (recipient doesn't support attachments). */ disabled?: boolean; /** Hint text shown when disabled and attachments are present. */ disabledHint?: string; } export const AttachmentPreviewList = ({ attachments, onRemove, error, onDismissError, disabled, disabledHint, }: AttachmentPreviewListProps): React.JSX.Element | null => { const [lightboxIndex, setLightboxIndex] = useState(null); const [exitingIds, setExitingIds] = useState>(new Set()); // Track IDs known on previous render to detect newly added items const knownIdsRef = useRef>(new Set()); const [enteringIds, setEnteringIds] = useState>(new Set()); const exitTimersRef = useRef>(new Map()); const enterTimersRef = useRef>(new Map()); // Detect newly added attachments useEffect(() => { const currentIds = new Set(attachments.map((a) => a.id)); const newIds = new Set(); for (const id of currentIds) { if (!knownIdsRef.current.has(id)) { newIds.add(id); } } knownIdsRef.current = currentIds; if (newIds.size === 0) return; queueMicrotask(() => { setEnteringIds((prev) => { const next = new Set(prev); for (const id of newIds) next.add(id); return next; }); }); // Clear entering state after animation completes for (const id of newIds) { const timer = window.setTimeout(() => { setEnteringIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); enterTimersRef.current.delete(id); }, ANIMATION_MS); enterTimersRef.current.set(id, timer); } }, [attachments]); // Cleanup timers on unmount useEffect(() => { const exitTimers = exitTimersRef.current; const enterTimers = enterTimersRef.current; return () => { for (const t of exitTimers.values()) window.clearTimeout(t); for (const t of enterTimers.values()) window.clearTimeout(t); }; }, []); const handleRemove = useCallback( (id: string) => { // Start exit animation setExitingIds((prev) => new Set(prev).add(id)); // Actually remove after animation const timer = window.setTimeout(() => { setExitingIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); exitTimersRef.current.delete(id); onRemove(id); }, ANIMATION_MS); exitTimersRef.current.set(id, timer); }, [onRemove] ); // Include exiting items that are no longer in attachments (they were removed by parent) // This shouldn't normally happen since we delay onRemove, but guard against it. const visibleAttachments = attachments; if (visibleAttachments.length === 0 && exitingIds.size === 0 && !error) return null; const lightboxSlides = visibleAttachments.map((att) => ({ src: `data:${att.mimeType};base64,${att.data}`, alt: att.filename, })); return (
{visibleAttachments.length > 0 ? (
{visibleAttachments.map((att, i) => { const isExiting = exitingIds.has(att.id); const isEntering = enteringIds.has(att.id); return (
setLightboxIndex(i)} disabled={disabled} />
); })}
) : null} {disabled && disabledHint && visibleAttachments.length > 0 ? (

{disabledHint}

) : null} {error ? (

{error}

{onDismissError ? ( ) : null}
) : null} {lightboxIndex !== null && lightboxSlides[lightboxIndex] ? ( setLightboxIndex(null)} slides={lightboxSlides} index={lightboxIndex} /> ) : null}
); };