diff --git a/js/app.js b/js/app.js index 0aa4c9c8..f1959c2d 100644 --- a/js/app.js +++ b/js/app.js @@ -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.", diff --git a/js/channelProfile.js b/js/channelProfile.js index 02cea144..08f13623 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -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"; @@ -3109,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 = ""; } diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index 523ae20d..b5dfc0c5 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -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}).` diff --git a/js/utils/formatters.js b/js/utils/formatters.js index 020e0150..a2af7f95 100644 --- a/js/utils/formatters.js +++ b/js/utils/formatters.js @@ -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. diff --git a/tests/profile-modal-controller.test.mjs b/tests/profile-modal-controller.test.mjs index 7bd5a1ab..dbc259bd 100644 --- a/tests/profile-modal-controller.test.mjs +++ b/tests/profile-modal-controller.test.mjs @@ -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 () => {