mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge branch 'unstable' into codex/fix-theme-color-issues-on-channel-view
This commit is contained in:
8 files changed
+394
-66
No files matched your search
@@ -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
|
||||
@@ -2223,7 +2223,6 @@
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.notification-portal {
|
||||
align-items: flex-end;
|
||||
padding-inline: var(--space-xl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 = "";
|
||||
}
|
||||
|
||||
@@ -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}).`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user