From 6ba44dd2d1e1a1de540fd3345a03652688c228af Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 17 Oct 2025 23:33:31 -0400 Subject: [PATCH 1/4] Fix channel banner preload policy --- js/channelProfile.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js/channelProfile.js b/js/channelProfile.js index e65ba999..2930bea5 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -161,6 +161,12 @@ function preloadBannerImage(url) { if ("loading" in img) { img.loading = img.loading || "eager"; } + if ("referrerPolicy" in img && !img.referrerPolicy) { + img.referrerPolicy = "no-referrer"; + } + if ("crossOrigin" in img && !img.crossOrigin) { + img.crossOrigin = "anonymous"; + } img.onload = () => { resolve(url); }; From 7594902f0bdb1afe2b2a63991656ac13d6d55094 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 17 Oct 2025 23:39:35 -0400 Subject: [PATCH 2/4] Handle channel banner load failures --- js/channelProfile.js | 42 +++++++++++++++++++++++++++++++++++--- views/channel-profile.html | 1 - 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index 2930bea5..b90a2d59 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -164,9 +164,6 @@ function preloadBannerImage(url) { if ("referrerPolicy" in img && !img.referrerPolicy) { img.referrerPolicy = "no-referrer"; } - if ("crossOrigin" in img && !img.crossOrigin) { - img.crossOrigin = "anonymous"; - } img.onload = () => { resolve(url); }; @@ -207,6 +204,43 @@ function setBannerVisual(el, url) { } } +function ensureBannerFallbackHandler(bannerEl) { + if (!bannerEl) { + return; + } + + const tagName = bannerEl.tagName ? bannerEl.tagName.toLowerCase() : ""; + if (tagName !== "img") { + return; + } + + if (bannerEl.dataset.bannerFallbackAttached === "true") { + return; + } + + const handleError = () => { + const fallbackAttr = + (typeof bannerEl.dataset?.fallbackSrc === "string" + ? bannerEl.dataset.fallbackSrc.trim() + : "") || + bannerEl.getAttribute("data-fallback-src") || + ""; + const fallbackSrc = fallbackAttr || FALLBACK_CHANNEL_BANNER; + + const activeSrc = bannerEl.currentSrc || bannerEl.src || ""; + if (activeSrc) { + channelBannerImageCache.delete(activeSrc); + } + + if (fallbackSrc && bannerEl.src !== fallbackSrc) { + bannerEl.src = fallbackSrc; + } + }; + + bannerEl.addEventListener("error", handleError, { passive: true }); + bannerEl.dataset.bannerFallbackAttached = "true"; +} + function getChannelZapButton() { if (cachedZapButton && !document.body.contains(cachedZapButton)) { cachedZapButton = null; @@ -2843,6 +2877,8 @@ function applyChannelProfileMetadata({ bannerEl.setAttribute("data-fallback-src", fallbackSrc); } + ensureBannerFallbackHandler(bannerEl); + if (!normalized.banner) { setBannerVisual(bannerEl, fallbackSrc); } else { diff --git a/views/channel-profile.html b/views/channel-profile.html index ba77fd80..9856e8c8 100644 --- a/views/channel-profile.html +++ b/views/channel-profile.html @@ -9,7 +9,6 @@ alt="Banner" loading="lazy" referrerpolicy="no-referrer" - crossorigin="anonymous" data-fallback-src="assets/jpg/bitvid.jpg" />
From 9ce27ffccfb78a91280c2354e4779eeb5b7992db Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 17 Oct 2025 23:48:56 -0400 Subject: [PATCH 3/4] Retry channel banners with relaxed referrers --- js/channelProfile.js | 153 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 128 insertions(+), 25 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index b90a2d59..8c9b21f6 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -38,7 +38,7 @@ import { requestInvoice } from "./payments/lnurl.js"; import { getPlatformLightningAddress } from "./payments/platformAddress.js"; -import { userLogger } from "./utils/logger.js"; +import { devLogger, userLogger } from "./utils/logger.js"; import { ensureWallet, sendPayment as sendWalletPayment @@ -144,13 +144,29 @@ const PROFILE_EVENT_CACHE_TTL_MS = 5 * 60 * 1000; const channelProfileMetadataCache = new Map(); const channelBannerImageCache = new Map(); -function preloadBannerImage(url) { +const DEFAULT_BANNER_REFERRER_POLICY_SEQUENCE = ["no-referrer", null]; + +function getBannerCacheKey(url, referrerPolicy) { + const keyPolicy = + typeof referrerPolicy === "string" && referrerPolicy + ? referrerPolicy + : "default"; + return `${keyPolicy}::${url}`; +} + +function preloadBannerImage(url, { referrerPolicy = "no-referrer" } = {}) { if (!url) { return Promise.reject(new Error("Invalid banner URL")); } - if (channelBannerImageCache.has(url)) { - return channelBannerImageCache.get(url); + const normalizedPolicy = + typeof referrerPolicy === "string" && referrerPolicy + ? referrerPolicy + : null; + const cacheKey = getBannerCacheKey(url, normalizedPolicy); + + if (channelBannerImageCache.has(cacheKey)) { + return channelBannerImageCache.get(cacheKey); } const promise = new Promise((resolve, reject) => { @@ -161,8 +177,15 @@ function preloadBannerImage(url) { if ("loading" in img) { img.loading = img.loading || "eager"; } - if ("referrerPolicy" in img && !img.referrerPolicy) { - img.referrerPolicy = "no-referrer"; + if (normalizedPolicy && "referrerPolicy" in img) { + try { + img.referrerPolicy = normalizedPolicy; + } catch (error) { + devLogger.warn( + "[channelProfile.banner] Failed to set preload referrer policy", + error + ); + } } img.onload = () => { resolve(url); @@ -176,11 +199,11 @@ function preloadBannerImage(url) { img.src = url; }); - channelBannerImageCache.set(url, promise); + channelBannerImageCache.set(cacheKey, promise); return promise; } -function setBannerVisual(el, url) { +function setBannerVisual(el, url, { referrerPolicy } = {}) { if (!el) { return; } @@ -189,6 +212,34 @@ function setBannerVisual(el, url) { const tagName = el.tagName ? el.tagName.toLowerCase() : ""; if (tagName === "img") { + if (referrerPolicy !== undefined) { + const normalizedPolicy = + referrerPolicy === null + ? "" + : typeof referrerPolicy === "string" + ? referrerPolicy + : ""; + + if ("referrerPolicy" in el) { + try { + el.referrerPolicy = normalizedPolicy; + } catch (error) { + devLogger.warn( + "[channelProfile.banner] Failed to set banner referrer policy", + error + ); + } + } + + if (normalizedPolicy) { + el.setAttribute("referrerpolicy", normalizedPolicy); + } else { + el.removeAttribute("referrerpolicy"); + } + + el.dataset.bannerReferrerPolicy = normalizedPolicy; + } + if (el.src !== resolvedUrl) { el.src = resolvedUrl; } @@ -214,6 +265,11 @@ function ensureBannerFallbackHandler(bannerEl) { return; } + if (!bannerEl.dataset.bannerReferrerPolicy) { + const initialPolicy = bannerEl.getAttribute("referrerpolicy") || ""; + bannerEl.dataset.bannerReferrerPolicy = initialPolicy; + } + if (bannerEl.dataset.bannerFallbackAttached === "true") { return; } @@ -229,11 +285,13 @@ function ensureBannerFallbackHandler(bannerEl) { const activeSrc = bannerEl.currentSrc || bannerEl.src || ""; if (activeSrc) { - channelBannerImageCache.delete(activeSrc); + channelBannerImageCache.delete( + getBannerCacheKey(activeSrc, bannerEl.dataset.bannerReferrerPolicy) + ); } if (fallbackSrc && bannerEl.src !== fallbackSrc) { - bannerEl.src = fallbackSrc; + setBannerVisual(bannerEl, fallbackSrc, { referrerPolicy: null }); } }; @@ -241,6 +299,59 @@ function ensureBannerFallbackHandler(bannerEl) { bannerEl.dataset.bannerFallbackAttached = "true"; } +function applyBannerWithPolicies({ + bannerEl, + url, + fallbackSrc, + policies = DEFAULT_BANNER_REFERRER_POLICY_SEQUENCE, + loadToken +} = {}) { + if (!bannerEl) { + return; + } + + const attemptPolicyAtIndex = (index) => { + if (index >= policies.length) { + if ( + Number.isInteger(loadToken) && + loadToken !== currentProfileLoadToken + ) { + return; + } + setBannerVisual(bannerEl, fallbackSrc, { referrerPolicy: null }); + return; + } + + const policy = policies[index]; + + preloadBannerImage(url, { referrerPolicy: policy }) + .then(() => { + if ( + Number.isInteger(loadToken) && + loadToken !== currentProfileLoadToken + ) { + return; + } + + setBannerVisual(bannerEl, url, { referrerPolicy: policy }); + }) + .catch(() => { + channelBannerImageCache.delete(getBannerCacheKey(url, policy)); + + if ( + Number.isInteger(loadToken) && + loadToken !== currentProfileLoadToken + ) { + return; + } + + attemptPolicyAtIndex(index + 1); + }); + }; + + attemptPolicyAtIndex(0); +} + function getChannelZapButton() { if (cachedZapButton && !document.body.contains(cachedZapButton)) { cachedZapButton = null; @@ -2880,22 +2991,14 @@ function applyChannelProfileMetadata({ ensureBannerFallbackHandler(bannerEl); if (!normalized.banner) { - setBannerVisual(bannerEl, fallbackSrc); + setBannerVisual(bannerEl, fallbackSrc, { referrerPolicy: null }); } else { - preloadBannerImage(normalized.banner) - .then(() => { - if (expectedLoadToken !== currentProfileLoadToken) { - return; - } - setBannerVisual(bannerEl, normalized.banner); - }) - .catch(() => { - channelBannerImageCache.delete(normalized.banner); - if (expectedLoadToken !== currentProfileLoadToken) { - return; - } - setBannerVisual(bannerEl, fallbackSrc); - }); + applyBannerWithPolicies({ + bannerEl, + url: normalized.banner, + fallbackSrc, + loadToken: expectedLoadToken + }); } } From b34f20685525c4e82c9b5130e2bf0ff901d26614 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 09:27:32 -0400 Subject: [PATCH 4/4] Improve channel banner loading retries --- js/channelProfile.js | 284 ++++++++++++++++++++++++++++--------------- 1 file changed, 189 insertions(+), 95 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index 8c9b21f6..02cea144 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -142,65 +142,150 @@ const FALLBACK_CHANNEL_AVATAR = "assets/svg/default-profile.svg"; const PROFILE_EVENT_CACHE_TTL_MS = 5 * 60 * 1000; const channelProfileMetadataCache = new Map(); -const channelBannerImageCache = new Map(); -const DEFAULT_BANNER_REFERRER_POLICY_SEQUENCE = ["no-referrer", null]; +const SUPPORTED_BANNER_REFERRER_POLICIES = new Set([ + "no-referrer", + "origin", + "origin-when-cross-origin", + "same-origin", + "strict-origin", + "strict-origin-when-cross-origin", + "unsafe-url" +]); -function getBannerCacheKey(url, referrerPolicy) { - const keyPolicy = - typeof referrerPolicy === "string" && referrerPolicy - ? referrerPolicy - : "default"; - return `${keyPolicy}::${url}`; +const DEFAULT_BANNER_REFERRER_POLICY_SEQUENCE = [ + "no-referrer", + "strict-origin-when-cross-origin", + "origin-when-cross-origin", + "unsafe-url" +]; + +const bannerLoadStates = new WeakMap(); + +function resolveToAbsoluteUrl(url) { + if (typeof url !== "string") { + return ""; + } + + const trimmed = url.trim(); + if (!trimmed) { + return ""; + } + + try { + if (typeof window !== "undefined" && window.location) { + return new URL(trimmed, window.location.href).href; + } + return new URL(trimmed).href; + } catch (error) { + return trimmed; + } } -function preloadBannerImage(url, { referrerPolicy = "no-referrer" } = {}) { - if (!url) { - return Promise.reject(new Error("Invalid banner URL")); +function normalizeReferrerPolicy(policy) { + if (policy === null) { + return null; } - const normalizedPolicy = - typeof referrerPolicy === "string" && referrerPolicy - ? referrerPolicy - : null; - const cacheKey = getBannerCacheKey(url, normalizedPolicy); - - if (channelBannerImageCache.has(cacheKey)) { - return channelBannerImageCache.get(cacheKey); + if (typeof policy !== "string") { + return undefined; } - const promise = new Promise((resolve, reject) => { - const img = new Image(); - if ("decoding" in img) { - img.decoding = img.decoding || "async"; - } - if ("loading" in img) { - img.loading = img.loading || "eager"; - } - if (normalizedPolicy && "referrerPolicy" in img) { - try { - img.referrerPolicy = normalizedPolicy; - } catch (error) { - devLogger.warn( - "[channelProfile.banner] Failed to set preload referrer policy", - error - ); - } - } - img.onload = () => { - resolve(url); - }; - img.onerror = (event) => { - const error = - (event && "error" in event && event.error) || - new Error("Failed to load banner image"); - reject(error); - }; - img.src = url; - }); + const trimmed = policy.trim(); + if (!trimmed) { + return null; + } - channelBannerImageCache.set(cacheKey, promise); - return promise; + if (trimmed.toLowerCase() === "default") { + return null; + } + + if (!SUPPORTED_BANNER_REFERRER_POLICIES.has(trimmed)) { + return undefined; + } + + return trimmed; +} + +function buildBannerPolicySequence(policies) { + const normalized = []; + const source = Array.isArray(policies) ? policies : []; + const seen = new Set(); + + for (const entry of source) { + if (entry === null) { + continue; + } + if (typeof entry !== "string") { + continue; + } + const trimmed = entry.trim(); + if (!trimmed || trimmed.toLowerCase() === "default") { + continue; + } + if (!SUPPORTED_BANNER_REFERRER_POLICIES.has(trimmed)) { + continue; + } + if (seen.has(trimmed)) { + continue; + } + normalized.push(trimmed); + seen.add(trimmed); + } + + normalized.push(null); + return normalized; +} + +function attemptBannerPolicy(bannerEl) { + const state = bannerLoadStates.get(bannerEl); + if (!state) { + return false; + } + + if (state.loadToken !== currentProfileLoadToken) { + return false; + } + + if (!state.url) { + bannerLoadStates.delete(bannerEl); + if (state.fallbackSrc) { + setBannerVisual(bannerEl, state.fallbackSrc, { referrerPolicy: null }); + } else { + setBannerVisual(bannerEl, "", { referrerPolicy: null }); + } + return false; + } + + while (state.attemptIndex < state.policies.length) { + const candidate = state.policies[state.attemptIndex]; + state.attemptIndex += 1; + + const normalizedPolicy = normalizeReferrerPolicy(candidate); + if (normalizedPolicy === undefined) { + continue; + } + + bannerLoadStates.set(bannerEl, state); + setBannerVisual(bannerEl, state.url, { referrerPolicy: normalizedPolicy }); + return true; + } + + bannerLoadStates.delete(bannerEl); + if (state.url) { + devLogger.warn( + "[channelProfile.banner] Exhausted referrer policies for banner", + { url: state.url } + ); + } + + if (state.fallbackSrc) { + setBannerVisual(bannerEl, state.fallbackSrc, { referrerPolicy: null }); + } else { + setBannerVisual(bannerEl, "", { referrerPolicy: null }); + } + + return false; } function setBannerVisual(el, url, { referrerPolicy } = {}) { @@ -275,23 +360,44 @@ function ensureBannerFallbackHandler(bannerEl) { } const handleError = () => { + const state = bannerLoadStates.get(bannerEl); + if (state && state.loadToken !== currentProfileLoadToken) { + return; + } + + const activeSrc = bannerEl.currentSrc || bannerEl.src || ""; + const normalizedActive = resolveToAbsoluteUrl(activeSrc); + + if ( + state && + state.resolvedUrl && + normalizedActive && + normalizedActive === state.resolvedUrl + ) { + const retried = attemptBannerPolicy(bannerEl); + if (retried) { + return; + } + return; + } + const fallbackAttr = (typeof bannerEl.dataset?.fallbackSrc === "string" ? bannerEl.dataset.fallbackSrc.trim() : "") || bannerEl.getAttribute("data-fallback-src") || ""; - const fallbackSrc = fallbackAttr || FALLBACK_CHANNEL_BANNER; + const fallbackSrc = + (state && state.fallbackSrc) || fallbackAttr || FALLBACK_CHANNEL_BANNER; + const fallbackResolved = + (state && state.resolvedFallback) || resolveToAbsoluteUrl(fallbackSrc); - const activeSrc = bannerEl.currentSrc || bannerEl.src || ""; - if (activeSrc) { - channelBannerImageCache.delete( - getBannerCacheKey(activeSrc, bannerEl.dataset.bannerReferrerPolicy) - ); - } + bannerLoadStates.delete(bannerEl); - if (fallbackSrc && bannerEl.src !== fallbackSrc) { + if (fallbackSrc && normalizedActive !== fallbackResolved) { setBannerVisual(bannerEl, fallbackSrc, { referrerPolicy: null }); + } else if (!fallbackSrc) { + setBannerVisual(bannerEl, "", { referrerPolicy: null }); } }; @@ -310,46 +416,33 @@ function applyBannerWithPolicies({ return; } - const attemptPolicyAtIndex = (index) => { - if (index >= policies.length) { - if ( - Number.isInteger(loadToken) && - loadToken !== currentProfileLoadToken - ) { - return; - } - setBannerVisual(bannerEl, fallbackSrc, { referrerPolicy: null }); - return; - } + const normalizedUrl = + typeof url === "string" && url.trim() ? url.trim() : ""; + const fallbackCandidate = + (typeof fallbackSrc === "string" && fallbackSrc.trim()) || + FALLBACK_CHANNEL_BANNER; + const normalizedPolicies = buildBannerPolicySequence(policies); + const expectedToken = Number.isInteger(loadToken) + ? loadToken + : currentProfileLoadToken; - const policy = policies[index]; + if (!normalizedUrl) { + bannerLoadStates.delete(bannerEl); + setBannerVisual(bannerEl, fallbackCandidate, { referrerPolicy: null }); + return; + } - preloadBannerImage(url, { referrerPolicy: policy }) - .then(() => { - if ( - Number.isInteger(loadToken) && - loadToken !== currentProfileLoadToken - ) { - return; - } + bannerLoadStates.set(bannerEl, { + url: normalizedUrl, + resolvedUrl: resolveToAbsoluteUrl(normalizedUrl), + fallbackSrc: fallbackCandidate, + resolvedFallback: resolveToAbsoluteUrl(fallbackCandidate), + policies: normalizedPolicies, + attemptIndex: 0, + loadToken: expectedToken + }); - setBannerVisual(bannerEl, url, { referrerPolicy: policy }); - }) - .catch(() => { - channelBannerImageCache.delete(getBannerCacheKey(url, policy)); - - if ( - Number.isInteger(loadToken) && - loadToken !== currentProfileLoadToken - ) { - return; - } - - attemptPolicyAtIndex(index + 1); - }); - }; - - attemptPolicyAtIndex(0); + attemptBannerPolicy(bannerEl); } function getChannelZapButton() { @@ -2991,6 +3084,7 @@ function applyChannelProfileMetadata({ ensureBannerFallbackHandler(bannerEl); if (!normalized.banner) { + bannerLoadStates.delete(bannerEl); setBannerVisual(bannerEl, fallbackSrc, { referrerPolicy: null }); } else { applyBannerWithPolicies({