fix(chat): make cached media load on safari

This commit is contained in:
2026-07-01 20:13:56 +08:00
parent 8586e87ab6
commit c20769b6a3
11 changed files with 299 additions and 72 deletions
@@ -12,7 +12,7 @@
*/
import { ChevronLeft } from "lucide-react";
import Image from "next/image";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import {
isBrowserLocalChatImageSource,
@@ -39,11 +39,13 @@ export function FullscreenImageViewer({
onUnlockImagePaywall,
onClose,
}: FullscreenImageViewerProps) {
const { mediaUrl } = useCachedChatMediaUrl({
messageId,
remoteUrl: imageUrl,
kind: "image",
});
const { mediaUrl, isUsingCachedMedia, reportMediaError } =
useCachedChatMediaUrl({
messageId,
remoteUrl: imageUrl,
kind: "image",
});
const [imageErrorSrc, setImageErrorSrc] = useState<string | null>(null);
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
@@ -55,6 +57,15 @@ export function FullscreenImageViewer({
const src = normalizeChatImageSource(mediaUrl || imageUrl);
const shouldUseNativeImage = isBrowserLocalChatImageSource(src);
const hasImageError = imageErrorSrc === src;
const handleImageError = () => {
if (isUsingCachedMedia) {
reportMediaError();
return;
}
setImageErrorSrc(src);
};
if (imagePaywalled) {
return (
@@ -68,6 +79,7 @@ export function FullscreenImageViewer({
src={src}
alt=""
className={`${styles.nativePaywallImage} ${styles.paywallImage}`}
onError={handleImageError}
/>
) : (
<Image
@@ -76,6 +88,7 @@ export function FullscreenImageViewer({
fill
sizes="(max-width: 540px) 100vw, 540px"
className={styles.paywallImage}
onError={handleImageError}
/>
)}
<div className={styles.paywallScrim} aria-hidden="true" />
@@ -108,16 +121,14 @@ export function FullscreenImageViewer({
role="dialog"
aria-label="Fullscreen image"
>
{shouldUseNativeImage ? (
{hasImageError ? (
<div className="error">🖼</div>
) : shouldUseNativeImage ? (
<img
src={src}
alt=""
className={styles.viewerImage}
onError={(e) => {
e.currentTarget.style.display = "none";
(e.currentTarget.parentElement as HTMLElement).innerHTML =
'<div class="error">🖼</div>';
}}
onError={handleImageError}
/>
) : (
<Image
@@ -126,11 +137,7 @@ export function FullscreenImageViewer({
width={800}
height={800}
className={styles.viewerImage}
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none";
(e.currentTarget.parentElement as HTMLElement).innerHTML =
'<div class="error">🖼</div>';
}}
onError={handleImageError}
/>
)}
</div>
+18 -8
View File
@@ -34,16 +34,26 @@ export function ImageBubble({
imagePaywalled = false,
}: ImageBubbleProps) {
const router = useRouter();
const [error, setError] = useState(false);
const { mediaUrl } = useCachedChatMediaUrl({
messageId,
remoteUrl: imageUrl,
kind: "image",
});
const [errorSrc, setErrorSrc] = useState<string | null>(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;
@@ -73,7 +83,7 @@ export function ImageBubble({
className={styles.image}
src={src}
alt=""
onError={() => setError(true)}
onError={handleImageError}
/>
) : (
<Image
@@ -82,7 +92,7 @@ export function ImageBubble({
alt=""
width={240}
height={240}
onError={() => setError(true)}
onError={handleImageError}
/>
)}
{!error && imagePaywalled ? (
+19 -10
View File
@@ -27,12 +27,13 @@ export function VoiceBubble({
}: VoiceBubbleProps) {
const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [hasError, setHasError] = useState(false);
const { mediaUrl } = useCachedChatMediaUrl({
messageId,
remoteUrl: audioUrl,
kind: "audio",
});
const [errorMediaUrl, setErrorMediaUrl] = useState<string | null>(null);
const { mediaUrl, isUsingCachedMedia, reportMediaError } =
useCachedChatMediaUrl({
messageId,
remoteUrl: audioUrl,
kind: "audio",
});
useEffect(() => {
const audio = audioRef.current;
@@ -42,7 +43,11 @@ export function VoiceBubble({
const handlePause = () => setIsPlaying(false);
const handlePlay = () => setIsPlaying(true);
const handleError = () => {
setHasError(true);
if (isUsingCachedMedia) {
reportMediaError();
return;
}
setErrorMediaUrl(mediaUrl);
setIsPlaying(false);
};
@@ -56,7 +61,9 @@ export function VoiceBubble({
audio.removeEventListener("play", handlePlay);
audio.removeEventListener("error", handleError);
};
}, []);
}, [isUsingCachedMedia, mediaUrl, reportMediaError]);
const hasError = errorMediaUrl === mediaUrl;
const handleToggle = () => {
if (locked) return;
@@ -70,7 +77,7 @@ export function VoiceBubble({
}
void audio.play().catch(() => {
setHasError(true);
setErrorMediaUrl(mediaUrl);
setIsPlaying(false);
});
};
@@ -125,7 +132,9 @@ export function VoiceBubble({
</>
) : null}
</div>
{!locked ? <audio ref={audioRef} src={mediaUrl} preload="metadata" /> : null}
{!locked ? (
<audio ref={audioRef} src={mediaUrl} preload="metadata" />
) : null}
</div>
);
}