Merge branch 'unstable' into codex/fix-theme-color-issues-on-channel-view

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-18 10:05:12 -04:00
commit 68df764790
8 files changed
+394 -66

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
:root{--surface-base:var(--color-surface);--surface-alt:var(--color-surface-alt);--surface-raised:var(--color-card);--surface-raised-private:var(--color-card-private);--surface-raised-critical:var(--color-card-critical);--surface-panel:var(--color-panel);--surface-panel-hover:var(--color-panel-hover);--surface-sunken:var(--color-surface-sunken);--surface-muted:var(--color-surface-muted);--surface-veil:var(--color-overlay-veil);--surface-inverse:var(--color-page);--surface-overlay:var(--color-overlay);--surface-overlay-muted:var(--color-overlay-muted);--surface-overlay-strong:var(--color-overlay-strong);--surface-overlay-darker:var(--color-overlay-darker);--surface-overlay-black:var(--color-overlay-black);--surface-overlay-banner:var(--color-overlay-banner);--surface-overlay-panel:var(--color-overlay-panel);--surface-overlay-panel-soft:var(--color-overlay-panel-soft);--surface-overlay-panel-tint:var(--color-overlay-panel-tint);--surface-overlay-panel-glass:var(--color-overlay-panel-glass);--surface-overlay-panel-glass-soft:var(--color-overlay-panel-glass-soft);--surface-modal-backdrop:var(--color-modal-backdrop);--surface-modal:var(--color-modal-surface);--blur-modal-backdrop:18px;--blur-modal-alert:14px;--blur-thumbnail-blur:18px;--modal-nav-backdrop-blur:6px;--border-default:var(--color-border);--border-subtle:var(--color-border-subtle);--border-strong:var(--color-border-translucent-stronger);--border-overlay:var(--color-border-translucent);--border-overlay-strong:var(--color-border-translucent-strongest);--border-overlay-glow:var(--color-border-translucent-glow);--border-neutral-soft:var(--color-border-neutral-soft);--border-info-soft:var(--color-border-info-soft);--border-sidebar-strong:var(--color-border-sidebar-strong);--border-width-hairline:1px;--border-width-fine:0.5px;--text-primary:var(--color-text);--text-inverse:var(--color-text-inverse);--text-muted:var(--color-text-muted);--text-muted-strong:var(--color-muted-strong);--text-subtle:var(--color-subtle-text);--text-frosted-primary:var(--color-text-frosted);--text-frosted-strong:var(--color-text-frosted-strong);--text-frosted-soft:var(--color-text-frosted-soft);--status-info:var(--color-info);--status-info-strong:var(--color-info-strong);--status-info-surface:var(--color-info-glass);--status-info-border:var(--color-info-border);--status-info-on:var(--color-info-on);--status-success:var(--color-success);--status-success-strong:var(--color-success-strong);--status-success-surface:var(--color-success-surface);--status-success-border:var(--color-success-border);--status-success-on:var(--color-success-on);--status-warning:var(--color-warning);--status-warning-strong:var(--color-warning-strong);--status-warning-surface:var(--color-warning-surface);--status-warning-border:var(--color-warning-border);--status-warning-on:var(--color-warning-on);--status-danger:var(--color-critical);--status-danger-strong:var(--color-critical-strong);--status-danger-surface:var(--color-danger-surface);--status-danger-border:var(--color-danger-border);--status-danger-on:var(--color-danger-on);--status-private:var(--color-private);--status-private-strong:var(--color-private-strong);--status-private-surface:var(--color-private-surface);--status-private-border:var(--color-private-border);--status-private-on:var(--color-private-on);--status-neutral:var(--color-muted);--status-neutral-strong:var(--color-muted-strong);--status-neutral-surface:var(--color-neutral-surface);--status-neutral-border:var(--color-neutral-border);--status-neutral-on:var(--color-neutral-on);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-overlay-nav:60;--z-overlay-floating:70;--z-overlay-popover:140;--z-overlay-toast:180;--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--overlay-backdrop-color:var(--color-overlay-backdrop);--overlay-backdrop-blur:12px;--sidebar-width-expanded:16rem;--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-lg);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-popover-padding-inline:0.65rem;--space-sidebar-popover-gap:0.45rem;--space-sidebar-dropup-scroll-reserve:calc(var(--scrollbar-thin-width) + var(--space-xs) + var(--space-2xs)/2);--sidebar-dropup-max-height:18rem;--space-sidebar-nav-padding-block:0.7rem;--space-sidebar-subnav-padding-block:0.55rem;--space-sidebar-nav-translate:0.35rem;--grid-caLine truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:100%}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar,body.modal-open #sidebarOverlay{pointer-events:none;opacity:0;visibility:hidden}body.modal-open #sidebar{z-index:var(--z-sidebar-overlay)}body.modal-open #sidebarOverlay{z-index:calc(var(--z-sidebar-overlay) - 1)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:flex}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.ds-metric-probe{position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;contain:layout size style;inline-size:0;block-size:var(--ds-metric-probe-length,0);overflow:hidden}.ds-header__logo{height:var(--space-2xl-plus);width:auto;max-width:none}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0Line truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:100%}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar,body.modal-open #sidebarOverlay{pointer-events:none;opacity:0;visibility:hidden}body.modal-open #sidebar{z-index:var(--z-sidebar-overlay)}body.modal-open #sidebarOverlay{z-index:calc(var(--z-sidebar-overlay) - 1)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:flex}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.ds-metric-probe{position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;contain:layout size style;inline-size:0;block-size:var(--ds-metric-probe-length,0);overflow:hidden}.ds-header__logo{height:var(--space-2xl-plus);width:auto;max-width:none}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0Line truncated
-1
View File
@@ -2223,7 +2223,6 @@
@media (min-width: 640px) {
.notification-portal {
align-items: flex-end;
padding-inline: var(--space-xl);
}
}
+6 -7
View File
@@ -62,6 +62,7 @@ import {
formatAbsoluteTimestamp as formatAbsoluteTimestampUtil,
formatTimeAgo as formatTimeAgoUtil,
truncateMiddle,
formatShortNpub,
} from "./utils/formatters.js";
import {
escapeHTML as escapeHtml,
@@ -436,6 +437,7 @@ class Application {
formatTimeAgo: (timestamp) => this.formatTimeAgo(timestamp),
escapeHTML: (value) => this.escapeHTML(value),
truncateMiddle,
formatShortNpub: (value) => formatShortNpub(value),
fallbackThumbnailSrc: FALLBACK_THUMBNAIL_SRC,
container: document.getElementById("modalContainer") || null,
});
@@ -456,6 +458,7 @@ class Application {
safeEncodeNpub: (pubkey) => this.safeEncodeNpub(pubkey),
safeDecodeNpub: (npub) => this.safeDecodeNpub(npub),
truncateMiddle: (value, maxLength) => truncateMiddle(value, maxLength),
formatShortNpub: (value) => formatShortNpub(value),
getProfileCacheEntry: (pubkey) => this.getProfileCacheEntry(pubkey),
batchFetchProfiles: (authorSet) => this.batchFetchProfiles(authorSet),
switchProfile: (pubkey) => this.authService.switchProfile(pubkey),
@@ -4504,18 +4507,14 @@ class Application {
if (typeof window !== "undefined" && window.NostrTools?.nip19?.npubEncode) {
const encoded = window.NostrTools.nip19.npubEncode(trimmed);
if (encoded && typeof encoded === "string") {
return `${encoded.slice(0, 8)}…${encoded.slice(-4)}`;
return formatShortNpub(encoded);
}
}
} catch (error) {
userLogger.warn("[Application] Failed to encode reporter npub", error);
}
if (trimmed.length > 12) {
return `${trimmed.slice(0, 8)}…${trimmed.slice(-4)}`;
}
return trimmed;
return formatShortNpub(trimmed);
}
decorateVideoModeration(video) {
@@ -6656,7 +6655,7 @@ class Application {
postedAt: this.currentVideo?.rootCreatedAt ?? null,
editedAt: normalizedEditedAt,
});
const displayNpub = `${creatorNpub.slice(0, 8)}...${creatorNpub.slice(-4)}`;
const displayNpub = formatShortNpub(creatorNpub);
this.videoModal.updateMetadata({
title: video.title || "Untitled",
description: video.description || "No description available.",
+287 -46
View File
@@ -11,6 +11,7 @@ import { attachUrlHealthBadges } from "./urlHealthObserver.js";
import { accessControl } from "./accessControl.js";
import { getApplication } from "./applicationContext.js";
import { escapeHTML } from "./utils/domUtils.js";
import { formatShortNpub } from "./utils/formatters.js";
import createPopover from "./ui/overlay/popoverEngine.js";
import { VideoCard } from "./ui/components/VideoCard.js";
import { createChannelProfileMenuPanel } from "./ui/components/videoMenuRenderers.js";
@@ -38,7 +39,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
@@ -142,42 +143,153 @@ 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();
function preloadBannerImage(url) {
if (!url) {
return Promise.reject(new Error("Invalid banner URL"));
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"
]);
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 "";
}
if (channelBannerImageCache.has(url)) {
return channelBannerImageCache.get(url);
const trimmed = url.trim();
if (!trimmed) {
return "";
}
const promise = new Promise((resolve, reject) => {
const img = new Image();
if ("decoding" in img) {
img.decoding = img.decoding || "async";
try {
if (typeof window !== "undefined" && window.location) {
return new URL(trimmed, window.location.href).href;
}
if ("loading" in img) {
img.loading = img.loading || "eager";
}
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;
});
channelBannerImageCache.set(url, promise);
return promise;
return new URL(trimmed).href;
} catch (error) {
return trimmed;
}
}
function setBannerVisual(el, url) {
function normalizeReferrerPolicy(policy) {
if (policy === null) {
return null;
}
if (typeof policy !== "string") {
return undefined;
}
const trimmed = policy.trim();
if (!trimmed) {
return null;
}
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 } = {}) {
if (!el) {
return;
}
@@ -186,6 +298,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;
}
@@ -201,6 +341,111 @@ function setBannerVisual(el, url) {
}
}
function ensureBannerFallbackHandler(bannerEl) {
if (!bannerEl) {
return;
}
const tagName = bannerEl.tagName ? bannerEl.tagName.toLowerCase() : "";
if (tagName !== "img") {
return;
}
if (!bannerEl.dataset.bannerReferrerPolicy) {
const initialPolicy = bannerEl.getAttribute("referrerpolicy") || "";
bannerEl.dataset.bannerReferrerPolicy = initialPolicy;
}
if (bannerEl.dataset.bannerFallbackAttached === "true") {
return;
}
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 =
(state && state.fallbackSrc) || fallbackAttr || FALLBACK_CHANNEL_BANNER;
const fallbackResolved =
(state && state.resolvedFallback) || resolveToAbsoluteUrl(fallbackSrc);
bannerLoadStates.delete(bannerEl);
if (fallbackSrc && normalizedActive !== fallbackResolved) {
setBannerVisual(bannerEl, fallbackSrc, { referrerPolicy: null });
} else if (!fallbackSrc) {
setBannerVisual(bannerEl, "", { referrerPolicy: null });
}
};
bannerEl.addEventListener("error", handleError, { passive: true });
bannerEl.dataset.bannerFallbackAttached = "true";
}
function applyBannerWithPolicies({
bannerEl,
url,
fallbackSrc,
policies = DEFAULT_BANNER_REFERRER_POLICY_SEQUENCE,
loadToken
} = {}) {
if (!bannerEl) {
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;
if (!normalizedUrl) {
bannerLoadStates.delete(bannerEl);
setBannerVisual(bannerEl, fallbackCandidate, { referrerPolicy: null });
return;
}
bannerLoadStates.set(bannerEl, {
url: normalizedUrl,
resolvedUrl: resolveToAbsoluteUrl(normalizedUrl),
fallbackSrc: fallbackCandidate,
resolvedFallback: resolveToAbsoluteUrl(fallbackCandidate),
policies: normalizedPolicies,
attemptIndex: 0,
loadToken: expectedToken
});
attemptBannerPolicy(bannerEl);
}
function getChannelZapButton() {
if (cachedZapButton && !document.body.contains(cachedZapButton)) {
cachedZapButton = null;
@@ -2837,23 +3082,18 @@ function applyChannelProfileMetadata({
bannerEl.setAttribute("data-fallback-src", fallbackSrc);
}
ensureBannerFallbackHandler(bannerEl);
if (!normalized.banner) {
setBannerVisual(bannerEl, fallbackSrc);
bannerLoadStates.delete(bannerEl);
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
});
}
}
@@ -2870,10 +3110,11 @@ function applyChannelProfileMetadata({
const channelNpubEl = document.getElementById("channelNpub");
if (channelNpubEl) {
if (npub) {
channelNpubEl.textContent = npub;
channelNpubEl.textContent = formatShortNpub(npub);
} else if (pubkey) {
try {
channelNpubEl.textContent = window.NostrTools.nip19.npubEncode(pubkey);
const encoded = window.NostrTools.nip19.npubEncode(pubkey);
channelNpubEl.textContent = formatShortNpub(encoded);
} catch (error) {
channelNpubEl.textContent = "";
}
+39 -10
View File
@@ -89,6 +89,13 @@ const SERVICE_CONTRACT = [
"Utility used for shortening long identifiers in the modal without losing context.",
fallback: () => (value) => value,
},
{
key: "formatShortNpub",
type: "function",
description:
"Formats npub strings for display using the canonical short representation (npubXXXX…XXXX).",
fallback: () => (value) => (typeof value === "string" ? value : ""),
},
{
key: "getProfileCacheEntry",
type: "function",
@@ -617,6 +624,7 @@ export class ProfileModalController {
this.safeEncodeNpub = this.services.safeEncodeNpub;
this.safeDecodeNpub = this.services.safeDecodeNpub;
this.truncateMiddle = this.services.truncateMiddle;
this.formatShortNpub = this.services.formatShortNpub;
this.sendAdminListNotificationService =
typeof this.services.sendAdminListNotification === "function"
? this.services.sendAdminListNotification
@@ -1198,8 +1206,12 @@ export class ProfileModalController {
const activeMeta = activeEntry ? resolveMeta(activeEntry) : null;
const hasActiveProfile = Boolean(activeEntry && activeMeta);
const truncate = this.truncateMiddle || ((value) => value);
const formatNpub =
typeof this.formatShortNpub === "function"
? (value) => this.formatShortNpub(value)
: (value) => (typeof value === "string" ? value : "");
const activeNameFallback = activeMeta?.npub
? truncate(activeMeta.npub, 32)
? formatNpub(activeMeta.npub) || "Saved profile"
: "Saved profile";
const activeDisplayName = hasActiveProfile
? activeMeta?.name?.trim() || activeNameFallback
@@ -1225,7 +1237,8 @@ export class ProfileModalController {
if (this.profileNpub) {
if (hasActiveProfile && activeMeta?.npub) {
this.profileNpub.textContent = truncate(activeMeta.npub, 48);
const displayNpub = formatNpub(activeMeta.npub);
this.profileNpub.textContent = displayNpub || "npub unavailable";
} else if (hasActiveProfile) {
this.profileNpub.textContent = "npub unavailable";
} else {
@@ -1320,7 +1333,7 @@ export class ProfileModalController {
avatarImg.src = meta.picture || FALLBACK_PROFILE_AVATAR;
const cardDisplayName =
meta.name?.trim() ||
(meta.npub ? truncate(meta.npub, 32) : "Saved profile");
(meta.npub ? formatNpub(meta.npub) || "Saved profile" : "Saved profile");
avatarImg.alt = `${cardDisplayName} avatar`;
avatarSpan.appendChild(avatarImg);
@@ -1349,9 +1362,12 @@ export class ProfileModalController {
const npubSpan = document.createElement("span");
npubSpan.className = "break-all font-mono text-xs text-muted";
npubSpan.textContent = meta.npub
? truncate(meta.npub, 48)
: "npub unavailable";
if (meta.npub) {
const displayNpub = formatNpub(meta.npub);
npubSpan.textContent = displayNpub || "npub unavailable";
} else {
npubSpan.textContent = "npub unavailable";
}
metaSpan.append(topLine, nameSpan, npubSpan);
button.append(avatarSpan, metaSpan);
@@ -2740,6 +2756,11 @@ export class ProfileModalController {
const { onRemove, removeLabel = "Remove", confirmMessage, removable = true } =
options;
const formatNpub =
typeof this.formatShortNpub === "function"
? (value) => this.formatShortNpub(value)
: (value) => (typeof value === "string" ? value : "");
listEl.innerHTML = "";
const values = Array.isArray(entries) ? [...entries] : [];
values.sort((a, b) => a.localeCompare(b));
@@ -2760,7 +2781,8 @@ export class ProfileModalController {
const label = document.createElement("p");
label.className = "break-all text-sm font-medium text-primary";
label.textContent = npub;
const displayNpub = formatNpub(npub) || npub;
label.textContent = displayNpub;
item.appendChild(label);
if (removable && typeof onRemove === "function") {
@@ -2770,7 +2792,7 @@ export class ProfileModalController {
removeBtn.textContent = removeLabel;
removeBtn.addEventListener("click", () => {
if (confirmMessage) {
const message = confirmMessage.replace("{npub}", npub);
const message = confirmMessage.replace("{npub}", displayNpub);
if (!window.confirm(message)) {
return;
}
@@ -3395,9 +3417,16 @@ export class ProfileModalController {
const actorDisplay = this.normalizeNpubValue(actorNpub) || fallbackActor;
const isWhitelist = listType === "whitelist";
const formatNpub =
typeof this.formatShortNpub === "function"
? (value) => this.formatShortNpub(value)
: (value) => (typeof value === "string" ? value : "");
const displayTarget = formatNpub(normalizedTarget) || normalizedTarget;
const displayActor = formatNpub(actorDisplay) || actorDisplay;
const introLine = isWhitelist
? `Great news—your npub ${normalizedTarget} has been added to the bitvid whitelist by ${actorDisplay}.`
: `We wanted to let you know that your npub ${normalizedTarget} has been placed on the bitvid blacklist by ${actorDisplay}.`;
? `Great news—your npub ${displayTarget} has been added to the bitvid whitelist by ${displayActor}.`
: `We wanted to let you know that your npub ${displayTarget} has been placed on the bitvid blacklist by ${displayActor}.`;
const statusLine = isWhitelist
? `You now have full creator access across bitvid (${this.bitvidWebsiteUrl}).`
+22
View File
@@ -20,6 +20,28 @@ export function truncateMiddle(text, maxLength = 72) {
return `${text.slice(0, front)}${ellipsis}${text.slice(text.length - back)}`;
}
/**
* Formats an npub string using the canonical short display format used across
* the app (first 8 characters, ellipsis, last 4 characters). When the input is
* not an npub or is already short, the original value is returned.
*/
export function formatShortNpub(value) {
if (typeof value !== "string") {
return "";
}
const trimmed = value.trim();
if (!trimmed) {
return "";
}
if (!trimmed.startsWith("npub") || trimmed.length <= 12) {
return trimmed;
}
return `${trimmed.slice(0, 8)}...${trimmed.slice(-4)}`;
}
/**
* Formats a timestamp (in seconds) to a localized string. Falls back to
* ISO-8601 formatting if locale formatting fails.
+39
View File
@@ -3,6 +3,7 @@ import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { JSDOM } from 'jsdom';
import ProfileModalController from '../js/ui/profileModalController.js';
import { formatShortNpub } from '../js/utils/formatters.js';
import { resetRuntimeFlags } from '../js/constants.js';
import { applyDesignSystemAttributes } from '../js/designSystem.js';
@@ -289,6 +290,44 @@ for (const _ of [0]) {
});
},
);
test('Profile modal uses abbreviated npub display', async () => {
const sampleProfiles = [
{
pubkey: defaultActorHex,
npub: 'npub1abcdefghijkmnopqrstuvwxyz1234567890example',
name: '',
picture: '',
},
{
pubkey: 'b'.repeat(64),
npub: 'npub1zyxwvutsrqponmlkjihgfedcba1234567890sample',
name: '',
picture: '',
},
];
const controller = createController({
services: {
formatShortNpub,
},
});
await controller.load();
controller.state.setSavedProfiles(sampleProfiles);
controller.state.setActivePubkey(sampleProfiles[0].pubkey);
controller.renderSavedProfiles();
const expectedActive = formatShortNpub(sampleProfiles[0].npub);
assert.equal(controller.profileNpub.textContent, expectedActive);
const switcherNpubEl = controller.switcherList.querySelector('.font-mono');
assert.ok(switcherNpubEl, 'switcher should render the secondary profile');
const expectedSwitcher = formatShortNpub(sampleProfiles[1].npub);
assert.equal(switcherNpubEl.textContent, expectedSwitcher);
});
}
test('load() injects markup and caches expected elements', async () => {
-1
View File
@@ -9,7 +9,6 @@
alt="Banner"
loading="lazy"
referrerpolicy="no-referrer"
crossorigin="anonymous"
data-fallback-src="assets/jpg/bitvid.jpg"
/>
<div class="absolute inset-0 rounded-lg bg-black bg-opacity-50"></div>