Skip to content

Commit 7f233f8

Browse files
committed
feat: add loading spinner and reset state on imageUrl change in ImageBubble component
1 parent eb05488 commit 7f233f8

File tree

1 file changed

+35
-16
lines changed

1 file changed

+35
-16
lines changed

packages/webapp/src/components/chat/bubbles/ImageBubble.tsx

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function ImageBubble({
2525
const [inView, setInView] = useState(false)
2626
const [retryCount, setRetryCount] = useState(0)
2727
const [failed, setFailed] = useState(false)
28+
const [loaded, setLoaded] = useState(false)
2829
const retryTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined)
2930

3031
// Clean up pending retry timer on unmount.
@@ -76,10 +77,11 @@ export function ImageBubble({
7677
}, delay)
7778
}, [imageUrl, retryCount])
7879

79-
// Reset retry state when imageUrl changes (e.g. sender switches from blob to http).
80+
// Reset retry/loading state when imageUrl changes (e.g. sender switches from blob to http).
8081
useEffect(() => {
8182
setRetryCount(0)
8283
setFailed(false)
84+
setLoaded(false)
8385
}, [imageUrl])
8486

8587
const shouldLoad = inView || !imageUrl
@@ -104,21 +106,38 @@ export function ImageBubble({
104106
Image unavailable
105107
</div>
106108
) : (
107-
<Dialog>
108-
<DialogTrigger asChild>
109-
<img
110-
src={src}
111-
alt="image"
112-
loading="lazy"
113-
onError={handleError}
114-
className="block max-h-72 max-w-[280px] cursor-zoom-in rounded-[4px] object-cover"
115-
/>
116-
</DialogTrigger>
117-
<DialogContent className="max-w-4xl bg-black/95 p-2" aria-describedby={undefined}>
118-
<DialogTitle className="sr-only">Image preview</DialogTitle>
119-
<img src={src} alt="image-full" className="max-h-[85vh] w-full rounded object-contain" />
120-
</DialogContent>
121-
</Dialog>
109+
<>
110+
{/* Spinner overlay while loading */}
111+
{!loaded && (
112+
<div className="flex h-48 w-[280px] items-center justify-center rounded-[4px] bg-muted">
113+
<svg
114+
className="h-8 w-8 animate-spin text-muted-foreground"
115+
xmlns="http://www.w3.org/2000/svg"
116+
fill="none"
117+
viewBox="0 0 24 24"
118+
>
119+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
120+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
121+
</svg>
122+
</div>
123+
)}
124+
<Dialog>
125+
<DialogTrigger asChild>
126+
<img
127+
src={src}
128+
alt="image"
129+
loading="lazy"
130+
onLoad={() => setLoaded(true)}
131+
onError={handleError}
132+
className={`block max-h-72 max-w-[280px] cursor-zoom-in rounded-[4px] object-cover${loaded ? "" : " hidden"}`}
133+
/>
134+
</DialogTrigger>
135+
<DialogContent className="max-w-4xl bg-black/95 p-2" aria-describedby={undefined}>
136+
<DialogTitle className="sr-only">Image preview</DialogTitle>
137+
<img src={src} alt="image-full" className="max-h-[85vh] w-full rounded object-contain" />
138+
</DialogContent>
139+
</Dialog>
140+
</>
122141
)
123142
) : (
124143
<div className="h-48 w-[280px]" />

0 commit comments

Comments
 (0)