import { useEffect, useState } from 'react'; import { Icon } from '@/app/Icon'; import { useAuthedBlobUrl } from '@/lib/hooks'; import { fileTypeStyle, formatFileSize } from '@/lib/messages/attachments'; interface ImageData { val?: string; ref?: string; mime?: string; width?: number; height?: number; } export function ImageAttachment({ data }: { data: ImageData }) { const inline = typeof data.val === 'string' ? `data:${data.mime || 'image/jpeg'};base64,${data.val}` : null; const remote = useAuthedBlobUrl(inline ? undefined : data.ref); const src = inline || remote.url; if (!src) { return (
); } // README §3.2: fixed 260x170, not a same-aspect-ratio-preserving max-height/width box. return ; } interface FileData { val?: string; ref?: string; mime?: string; name?: string; size?: number; } export function FileAttachment({ data }: { data: FileData }) { const style = fileTypeStyle(data.name, data.mime); const remote = useAuthedBlobUrl(typeof data.val === 'string' ? undefined : data.ref); const inline = typeof data.val === 'string' ? `data:${data.mime || 'application/octet-stream'};base64,${data.val}` : null; const href = inline || remote.url || undefined; return ( { if (remote.error) { e.preventDefault(); remote.retry(); } else if (!href) { e.preventDefault(); } }} className="flex w-[280px] items-center gap-3 rounded-lg bg-white px-3.5 py-3 no-underline shadow-bubble" > {data.name || '文件'} {typeof data.size === 'number' ? formatFileSize(data.size) : ''} ·{' '} {remote.error ? '下载失败,点击重试' : remote.loading ? '加载中…' : '点击下载'} ); } interface VideoData { val?: string; ref?: string; mime?: string; preview?: string; duration?: number; } function formatDuration(ms: number | undefined): string { const totalSeconds = Math.round((ms || 0) / 1000); return `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, '0')}`; } /** README §3.2: 260x170 like ImageAttachment, `play_circle` overlay + duration badge, no controls * until tapped. The video's own `val`/`mime` describe the full video body (confirmed live via * favorites' identical `VD` entity shape, docs/design_handoff plan file Part D) -- not paintable as * an ``, so the static poster comes from the separate inline `preview` field (a real JPEG, * confirmed by its base64 signature) instead. Tapping swaps the poster for a real `