From ccd2d6bd48359034d17ea0ce3f80ba3ab8c4b36c Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 30 Jun 2026 15:59:33 +0800 Subject: [PATCH] feat(media): implement caching for chat media and update components to use cached media --- .../fullscreen-image-viewer.module.css | 7 + .../components/fullscreen-image-viewer.tsx | 83 ++++-- src/app/chat/components/image-bubble.tsx | 21 +- src/app/chat/components/message-content.tsx | 1 + src/app/chat/components/voice-bubble.tsx | 10 +- .../chat/hooks/use-cached-chat-media-url.ts | 106 ++++++++ .../[messageId]/chat-image-viewer-screen.tsx | 1 + src/data/repositories/chat_repository.ts | 244 +++++++++++++++++- .../interfaces/ichat_repository.ts | 33 +++ src/data/storage/chat/index.ts | 1 + src/data/storage/chat/local_chat_db.ts | 21 ++ .../storage/chat/local_chat_media_storage.ts | 88 +++++++ src/lib/auth/logout.ts | 15 -- src/stores/chat/chat-machine.actors.ts | 1 + src/stores/chat/chat-machine.helpers.ts | 1 + 15 files changed, 591 insertions(+), 42 deletions(-) create mode 100644 src/app/chat/hooks/use-cached-chat-media-url.ts create mode 100644 src/data/storage/chat/local_chat_media_storage.ts delete mode 100644 src/lib/auth/logout.ts diff --git a/src/app/chat/components/fullscreen-image-viewer.module.css b/src/app/chat/components/fullscreen-image-viewer.module.css index 08aa5c9a..1fd61097 100644 --- a/src/app/chat/components/fullscreen-image-viewer.module.css +++ b/src/app/chat/components/fullscreen-image-viewer.module.css @@ -30,6 +30,13 @@ transform: scale(1.08); } +.nativePaywallImage { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + .paywallScrim { position: absolute; inset: 0; diff --git a/src/app/chat/components/fullscreen-image-viewer.tsx b/src/app/chat/components/fullscreen-image-viewer.tsx index 3b4d8088..e0b861de 100644 --- a/src/app/chat/components/fullscreen-image-viewer.tsx +++ b/src/app/chat/components/fullscreen-image-viewer.tsx @@ -1,4 +1,5 @@ "use client"; +/* eslint-disable @next/next/no-img-element */ /** * FullscreenImageViewer 全屏图片查看器 * @@ -13,9 +14,11 @@ import { ChevronLeft } from "lucide-react"; import Image from "next/image"; import { useEffect } from "react"; +import { useCachedChatMediaUrl } from "../hooks/use-cached-chat-media-url"; import styles from "./fullscreen-image-viewer.module.css"; export interface FullscreenImageViewerProps { + messageId?: string; imageUrl: string; imagePaywalled?: boolean; onUnlockImagePaywall?: () => void; @@ -23,11 +26,18 @@ export interface FullscreenImageViewerProps { } export function FullscreenImageViewer({ + messageId, imageUrl, imagePaywalled = false, onUnlockImagePaywall, onClose, }: FullscreenImageViewerProps) { + const { mediaUrl } = useCachedChatMediaUrl({ + messageId, + remoteUrl: imageUrl, + kind: "image", + }); + useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); @@ -36,9 +46,13 @@ export function FullscreenImageViewer({ return () => document.removeEventListener("keydown", handleKey); }, [onClose]); - const src = imageUrl.startsWith("data:") || imageUrl.startsWith("http") - ? imageUrl - : `data:image/png;base64,${imageUrl}`; + const sourceUrl = mediaUrl || imageUrl; + const src = sourceUrl.startsWith("data:") || + sourceUrl.startsWith("http") || + sourceUrl.startsWith("blob:") + ? sourceUrl + : `data:image/png;base64,${sourceUrl}`; + const shouldUseNativeImage = isBrowserLocalImageSrc(src); if (imagePaywalled) { return ( @@ -47,13 +61,21 @@ export function FullscreenImageViewer({ role="dialog" aria-label="Locked fullscreen image" > - + {shouldUseNativeImage ? ( + + ) : ( + + )} ); } + +function isBrowserLocalImageSrc(src: string): boolean { + return src.startsWith("blob:") || src.startsWith("data:"); +} diff --git a/src/app/chat/components/image-bubble.tsx b/src/app/chat/components/image-bubble.tsx index 8cf23a06..b650f8d7 100644 --- a/src/app/chat/components/image-bubble.tsx +++ b/src/app/chat/components/image-bubble.tsx @@ -1,4 +1,5 @@ "use client"; +/* eslint-disable @next/next/no-img-element */ /** * ImageBubble 图片气泡 * @@ -13,6 +14,7 @@ import { useState } from "react"; import { ROUTE_BUILDERS } from "@/router/routes"; +import { useCachedChatMediaUrl } from "../hooks/use-cached-chat-media-url"; import styles from "./image-bubble.module.css"; export interface ImageBubbleProps { @@ -28,9 +30,15 @@ export function ImageBubble({ }: ImageBubbleProps) { const router = useRouter(); const [error, setError] = useState(false); + const { mediaUrl } = useCachedChatMediaUrl({ + messageId, + remoteUrl: imageUrl, + kind: "image", + }); - const src = decodeBase64Image(imageUrl); + const src = decodeBase64Image(mediaUrl || imageUrl); const canOpen = Boolean(messageId); + const shouldUseNativeImage = isBrowserLocalImageSrc(src); const openImage = () => { if (!messageId) return; @@ -54,6 +62,13 @@ export function ImageBubble({ > {error ? (
🖼
+ ) : shouldUseNativeImage ? ( + setError(true)} + /> ) : ( (null); const [isPlaying, setIsPlaying] = useState(false); const [hasError, setHasError] = useState(false); + const { mediaUrl } = useCachedChatMediaUrl({ + messageId, + remoteUrl: audioUrl, + kind: "audio", + }); useEffect(() => { const audio = audioRef.current; @@ -117,7 +125,7 @@ export function VoiceBubble({ ) : null} - {!locked ?