"use client"; /* eslint-disable @next/next/no-img-element */ /** * ImageBubble 图片气泡 * * 支持: * - base64 data URI 解码(`data:image/png;base64,...`) * - 点击 → 跳转动态路由打开全屏查看器 * - 错误占位符(broken image icon) */ import Image from "next/image"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { ROUTE_BUILDERS } from "@/router/routes"; import { isBrowserLocalChatImageSource, normalizeChatImageSource, } from "@/lib/chat/chat_media_url"; import { useCachedChatMediaUrl } from "@/lib/chat/use_cached_chat_media_url"; import { requestChatScrollSnapshotSave } from "../chat-scroll-session"; import styles from "./image-bubble.module.css"; export interface ImageBubbleProps { messageId?: string; imageUrl: string; // base64 data URI 或 URL imagePaywalled?: boolean; } export function ImageBubble({ messageId, imageUrl, imagePaywalled = false, }: ImageBubbleProps) { const router = useRouter(); const [errorSrc, setErrorSrc] = useState(null); const { mediaUrl, isUsingCachedMedia, reportMediaError } = useCachedChatMediaUrl({ messageId, remoteUrl: imageUrl, kind: "image", }); const src = normalizeChatImageSource(mediaUrl || imageUrl); const canOpen = Boolean(messageId); const shouldUseNativeImage = isBrowserLocalChatImageSource(src); const error = errorSrc === src; const handleImageError = () => { if (isUsingCachedMedia) { reportMediaError(); return; } setErrorSrc(src); }; const openImage = () => { if (!messageId) return; requestChatScrollSnapshotSave(messageId); router.push(ROUTE_BUILDERS.chatImage(messageId), { scroll: false }); }; return (
{ if (!canOpen) return; if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openImage(); } }} aria-label={canOpen ? "Open image in fullscreen" : undefined} > {error ? (
🖼
) : shouldUseNativeImage ? ( ) : ( )} {!error && imagePaywalled ? ( ); }