Files
eranos/src/lib/nsiteSubdomain.ts
T
Alex Gleason 9e6ed02ce1 Add nsite:// sidebar pinning with auto-launch, favicon, and highlight
Introduces a new sidebar item type for nsites that auto-opens the nsite
preview when clicked, using React Router state to prevent external URLs
from triggering auto-launch.

- Add isNsiteUri/nsiteUriToSubdomain helpers and parseNsiteSubdomain
- Create NsiteSidebarItem with site favicon and link preview title label
- Wire nsite:// dispatch in SidebarNavList, useFeedSettings, SidebarMoreMenu
- NoteMoreMenu pins named nsite events as nsite:// URIs instead of nostr:
- NsiteCard gains a Pin/Unpin button and an autoPlayKey prop that re-opens
  the player each time the sidebar item is clicked
- NsitePlayerContext tracks the active subdomain for sidebar highlighting,
  provided in MainLayout so sidebar and pages share state
- PostDetailContent consumes nsiteAutoPlay router state and clears it
  after consumption so a page refresh doesn't re-trigger auto-play
2026-05-05 12:38:26 -05:00

87 lines
2.6 KiB
TypeScript

import type { NostrEvent } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools';
/** The fixed length of a base36-encoded 32-byte pubkey. */
const BASE36_PUBKEY_LENGTH = 50;
/** Encode a 32-byte hex pubkey as a base36 string (50 chars, zero-padded). */
export function hexToBase36(hex: string): string {
let n = 0n;
for (let i = 0; i < hex.length; i++) {
n = n * 16n + BigInt(parseInt(hex[i], 16));
}
const b36 = n.toString(36);
return b36.padStart(BASE36_PUBKEY_LENGTH, '0');
}
/** Decode a base36-encoded pubkey back to a 64-char hex string. */
function base36ToHex(b36: string): string {
const n = [...b36].reduce((acc, ch) => acc * 36n + BigInt(parseInt(ch, 36)), 0n);
return n.toString(16).padStart(64, '0');
}
/**
* Parsed nsite subdomain — either a root site (kind 15128) or a named site (kind 35128).
*/
export interface ParsedNsiteSubdomain {
/** The hex pubkey of the site owner. */
pubkey: string;
/** The event kind (15128 for root, 35128 for named). */
kind: 15128 | 35128;
/** The d-tag identifier (empty string for root sites). */
identifier: string;
}
/**
* Parse an nsite subdomain back into its components.
*
* - Root site subdomain: `<npub1...>` → kind 15128, identifier ""
* - Named site subdomain: `<50-char-base36><dTag>` → kind 35128, identifier = dTag
*
* Returns null if the subdomain cannot be parsed.
*/
export function parseNsiteSubdomain(subdomain: string): ParsedNsiteSubdomain | null {
// Root site: subdomain is an npub
if (subdomain.startsWith('npub1')) {
try {
const decoded = nip19.decode(subdomain);
if (decoded.type !== 'npub') return null;
return { pubkey: decoded.data as string, kind: 15128, identifier: '' };
} catch {
return null;
}
}
// Named site: first 50 chars are base36 pubkey, rest is d-tag
if (subdomain.length <= BASE36_PUBKEY_LENGTH) return null;
const b36Part = subdomain.slice(0, BASE36_PUBKEY_LENGTH);
const dTag = subdomain.slice(BASE36_PUBKEY_LENGTH);
// Validate base36 characters
if (!/^[0-9a-z]+$/.test(b36Part)) return null;
try {
const pubkey = base36ToHex(b36Part);
return { pubkey, kind: 35128, identifier: dTag };
} catch {
return null;
}
}
/**
* Derive the NIP-5A canonical subdomain for an nsite event.
*
* - Root site (kind 15128): `<npub>`
* - Named site (kind 35128 with d-tag): `<pubkeyB36><dTag>`
*/
export function getNsiteSubdomain(event: NostrEvent): string {
const dTag = event.tags.find(([n]) => n === 'd')?.[1];
if (event.kind === 35128 && dTag) {
const pubkeyB36 = hexToBase36(event.pubkey);
return `${pubkeyB36}${dTag}`;
}
return nip19.npubEncode(event.pubkey);
}