diff --git a/src/app/chat/components/fullscreen-image-viewer.tsx b/src/app/chat/components/fullscreen-image-viewer.tsx index a253ecca..2cf8abb5 100644 --- a/src/app/chat/components/fullscreen-image-viewer.tsx +++ b/src/app/chat/components/fullscreen-image-viewer.tsx @@ -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(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} /> ) : ( )} diff --git a/src/app/chat/components/image-bubble.tsx b/src/app/chat/components/image-bubble.tsx index 1fb03393..506e1ba7 100644 --- a/src/app/chat/components/image-bubble.tsx +++ b/src/app/chat/components/image-bubble.tsx @@ -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(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} /> ) : ( setError(true)} + onError={handleImageError} /> )} {!error && imagePaywalled ? ( diff --git a/src/app/chat/components/voice-bubble.tsx b/src/app/chat/components/voice-bubble.tsx index 8e9cfb5b..fb0792a7 100644 --- a/src/app/chat/components/voice-bubble.tsx +++ b/src/app/chat/components/voice-bubble.tsx @@ -27,12 +27,13 @@ export function VoiceBubble({ }: VoiceBubbleProps) { const audioRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); - const [hasError, setHasError] = useState(false); - const { mediaUrl } = useCachedChatMediaUrl({ - messageId, - remoteUrl: audioUrl, - kind: "audio", - }); + const [errorMediaUrl, setErrorMediaUrl] = useState(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} - {!locked ?