fix(ui): a picture that fails to load shows the card's own frame, then retries
A bare <img> sent a Referer that Google's image CDNs refuse and had no retry and no fallback, so a Home shelf card showed the browser's broken-image mark. ui/RemoteImage gathers what Avatar, the channel banner and the download rows had each hand-rolled: no Referer, up to three retries past the browser's negative cache, the caller's fallback (by default the card's empty frame) instead of the broken mark, the failure remembered by URL. Every picture now goes through it; a lint rule refuses a bare <img>.
This commit is contained in:
@@ -39,6 +39,7 @@ import { Pressable } from "./ui/Pressable";
|
||||
import { Input } from "./ui/Input";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { LinkButton } from "./ui/LinkButton";
|
||||
import RemoteImage from "./ui/RemoteImage";
|
||||
|
||||
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
||||
const PASSWORD_MIN = 10;
|
||||
@@ -224,28 +225,18 @@ function Preview({
|
||||
className?: string;
|
||||
onOpen?: (src: string, label: string) => void;
|
||||
}) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
if (failed) {
|
||||
return (
|
||||
<div className={`${SURFACE.card} overflow-hidden ${className ?? ""}`}>
|
||||
<div className="w-full h-full grid place-items-center bg-linear-to-br from-accent/10 to-card text-muted text-sm p-6 text-center">
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Pressable
|
||||
onClick={() => onOpen?.(src, label)}
|
||||
aria-label={label}
|
||||
className={`group relative ${SURFACE.card} overflow-hidden block cursor-zoom-in ${className ?? ""}`}
|
||||
>
|
||||
<img
|
||||
<RemoteImage
|
||||
src={src}
|
||||
alt={label}
|
||||
loading="lazy"
|
||||
onError={() => setFailed(true)}
|
||||
className="w-full h-full object-cover object-top"
|
||||
fallback={<ShotLabel label={label} />}
|
||||
/>
|
||||
<span className="absolute inset-0 grid place-items-center bg-black/0 opacity-0 transition group-hover:bg-black/30 group-hover:opacity-100">
|
||||
<Expand className="w-6 h-6 text-white drop-shadow-lg" />
|
||||
@@ -254,6 +245,15 @@ function Preview({
|
||||
);
|
||||
}
|
||||
|
||||
// A screenshot that would not load: its caption on the card's own ground, not the broken-image mark.
|
||||
function ShotLabel({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="w-full h-full grid place-items-center bg-linear-to-br from-accent/10 to-card text-muted text-sm p-6 text-center">
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Full-size image viewer: a dimmed, blurred backdrop with the screenshot fit to the viewport
|
||||
// (aspect ratio preserved via object-contain). Closes on backdrop click, the ✕, or Escape.
|
||||
function Lightbox({ src, label, onClose }: { src: string; label: string; onClose: () => void }) {
|
||||
@@ -320,7 +320,7 @@ function Lightbox({ src, label, onClose }: { src: string; label: string; onClose
|
||||
className="flex max-h-full max-w-full flex-col items-center gap-3"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<img
|
||||
<RemoteImage
|
||||
src={src}
|
||||
alt={label}
|
||||
className="max-h-[82dvh] max-w-[92vw] w-auto h-auto rounded-xl border border-border object-contain shadow-2xl"
|
||||
|
||||
Reference in New Issue
Block a user