From a640b181eef5ccd649771ef695e21cbbb9e918b5 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 12 Jun 2026 18:01:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20robust=20avatars=20=E2=80=94=20no-re?= =?UTF-8?q?ferrer=20+=20graceful=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Channel/account avatars come from Google's image CDNs (yt3.ggpht.com, lh3.googleusercontent.com). On a feed page dozens load at once; the CDN rate-limits the referrer-bearing burst (429), so a random subset rendered the browser's broken-image icon (the URLs themselves are valid — verified 200). Add a shared that sets referrerPolicy="no-referrer" (which the CDNs serve without throttling) and falls back to a neutral initial placeholder on error instead of the broken-image icon. Use it for video-card, player, channel manager, header and settings avatars. --- frontend/src/components/Avatar.tsx | 48 +++++++++++++++++++++++ frontend/src/components/Channels.tsx | 11 +++--- frontend/src/components/Header.tsx | 13 +++--- frontend/src/components/PlayerModal.tsx | 7 ++-- frontend/src/components/SettingsPanel.tsx | 13 +++--- frontend/src/components/VideoCard.tsx | 14 +++---- 6 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/Avatar.tsx diff --git a/frontend/src/components/Avatar.tsx b/frontend/src/components/Avatar.tsx new file mode 100644 index 0000000..c7477b5 --- /dev/null +++ b/frontend/src/components/Avatar.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from "react"; + +// Avatar image with a graceful fallback. +// +// Uses referrerPolicy="no-referrer" because Google's avatar CDNs +// (yt3.ggpht.com / lh3.googleusercontent.com) rate-limit bursts of +// referrer-bearing requests — when a feed page fires dozens of avatar loads at +// once, a random subset gets throttled (HTTP 429) and the browser shows its +// broken-image icon. Dropping the referrer makes those CDNs serve the images. +// If a load still fails we render a neutral placeholder (optionally an initial) +// instead of the broken-image icon. +export default function Avatar({ + src, + alt = "", + fallback, + className = "", +}: { + src?: string | null; + alt?: string; + fallback?: string; // a name/title; its first letter is shown when there's no image + className?: string; +}) { + const [failed, setFailed] = useState(false); + useEffect(() => setFailed(false), [src]); + + if (src && !failed) { + return ( + {alt} setFailed(true)} + className={className} + /> + ); + } + + const initial = fallback?.trim()?.[0]?.toUpperCase() ?? ""; + return ( +
+ {initial} +
+ ); +} diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index ea073b6..f36afef 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -16,6 +16,7 @@ import { api, type ManagedChannel, type Tag } from "../lib/api"; import { formatEta } from "../lib/format"; import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; +import Avatar from "./Avatar"; export default function Channels({ canWrite, @@ -332,11 +333,11 @@ function ChannelRow({ - {c.thumbnail_url ? ( - - ) : ( -
- )} +