|
|
|
|
@ -17,26 +17,25 @@
@@ -17,26 +17,25 @@
|
|
|
|
|
function getPubkeyFromNIP21(parsed: ReturnType<typeof parseNIP21>): string | null { |
|
|
|
|
if (!parsed) return null; |
|
|
|
|
|
|
|
|
|
if (parsed.type === 'npub') { |
|
|
|
|
// npub decodes directly to pubkey |
|
|
|
|
try { |
|
|
|
|
// parsed.data is the bech32 string (e.g., "npub1..." or "nprofile1...") |
|
|
|
|
// We need to decode it to get the actual pubkey |
|
|
|
|
const decoded = nip19.decode(parsed.data); |
|
|
|
|
|
|
|
|
|
if (parsed.type === 'npub') { |
|
|
|
|
// npub decodes directly to pubkey (hex string) |
|
|
|
|
if (decoded.type === 'npub') { |
|
|
|
|
return String(decoded.data); |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
} else if (parsed.type === 'nprofile') { |
|
|
|
|
// nprofile decodes to object with pubkey property |
|
|
|
|
try { |
|
|
|
|
const decoded = nip19.decode(parsed.data); |
|
|
|
|
if (decoded.type === 'nprofile' && decoded.data && typeof decoded.data === 'object' && 'pubkey' in decoded.data) { |
|
|
|
|
return String(decoded.data.pubkey); |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error decoding NIP-21 URI:', error, parsed); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return null; |
|
|
|
|
@ -173,28 +172,84 @@
@@ -173,28 +172,84 @@
|
|
|
|
|
const renderedHtml = $derived(renderMarkdown(content)); |
|
|
|
|
|
|
|
|
|
// Mount ProfileBadge components after rendering |
|
|
|
|
$effect(() => { |
|
|
|
|
if (!containerRef || !renderedHtml) return; |
|
|
|
|
|
|
|
|
|
// Use a small delay to ensure DOM is updated |
|
|
|
|
const timeoutId = setTimeout(() => { |
|
|
|
|
function mountProfileBadges() { |
|
|
|
|
if (!containerRef) return; |
|
|
|
|
|
|
|
|
|
// Find all profile placeholders and mount ProfileBadge components |
|
|
|
|
const placeholders = containerRef.querySelectorAll('[data-nostr-profile]'); |
|
|
|
|
const placeholders = containerRef.querySelectorAll('[data-nostr-profile]:not([data-mounted])'); |
|
|
|
|
|
|
|
|
|
if (placeholders.length === 0) return; |
|
|
|
|
|
|
|
|
|
console.debug(`Mounting ${placeholders.length} ProfileBadge components`); |
|
|
|
|
|
|
|
|
|
placeholders.forEach((placeholder) => { |
|
|
|
|
const pubkey = placeholder.getAttribute('data-pubkey'); |
|
|
|
|
if (pubkey && !placeholder.hasAttribute('data-mounted')) { |
|
|
|
|
// Mark as mounted to avoid double-mounting |
|
|
|
|
if (pubkey && /^[0-9a-f]{64}$/i.test(pubkey)) { |
|
|
|
|
placeholder.setAttribute('data-mounted', 'true'); |
|
|
|
|
mountComponent(placeholder as HTMLElement, ProfileBadge as any, { pubkey }); |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Clear and mount component |
|
|
|
|
placeholder.innerHTML = ''; |
|
|
|
|
// Use inline mode for profile badges in markdown content |
|
|
|
|
const instance = mountComponent(placeholder as HTMLElement, ProfileBadge as any, { pubkey, inline: true }); |
|
|
|
|
|
|
|
|
|
if (!instance) { |
|
|
|
|
console.warn('ProfileBadge mount returned null', { pubkey }); |
|
|
|
|
// Fallback |
|
|
|
|
try { |
|
|
|
|
const npub = nip19.npubEncode(pubkey); |
|
|
|
|
placeholder.textContent = npub.slice(0, 12) + '...'; |
|
|
|
|
} catch { |
|
|
|
|
placeholder.textContent = pubkey.slice(0, 12) + '...'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error mounting ProfileBadge:', error, { pubkey }); |
|
|
|
|
// Show fallback |
|
|
|
|
try { |
|
|
|
|
const npub = nip19.npubEncode(pubkey); |
|
|
|
|
placeholder.textContent = npub.slice(0, 12) + '...'; |
|
|
|
|
} catch { |
|
|
|
|
placeholder.textContent = pubkey.slice(0, 12) + '...'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else if (pubkey) { |
|
|
|
|
console.warn('Invalid pubkey format:', pubkey); |
|
|
|
|
placeholder.textContent = pubkey.slice(0, 12) + '...'; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}, 0); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
$effect(() => { |
|
|
|
|
if (!containerRef || !renderedHtml) return; |
|
|
|
|
|
|
|
|
|
// Use requestAnimationFrame + setTimeout to ensure DOM is ready |
|
|
|
|
const frameId = requestAnimationFrame(() => { |
|
|
|
|
const timeoutId = setTimeout(() => { |
|
|
|
|
mountProfileBadges(); |
|
|
|
|
}, 150); |
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timeoutId); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return () => cancelAnimationFrame(frameId); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Also use MutationObserver to catch any placeholders added later |
|
|
|
|
$effect(() => { |
|
|
|
|
if (!containerRef) return; |
|
|
|
|
|
|
|
|
|
const observer = new MutationObserver(() => { |
|
|
|
|
mountProfileBadges(); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
observer.observe(containerRef, { |
|
|
|
|
childList: true, |
|
|
|
|
subtree: true |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return () => observer.disconnect(); |
|
|
|
|
}); |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<div |
|
|
|
|
@ -286,4 +341,12 @@
@@ -286,4 +341,12 @@
|
|
|
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
|
|
color: var(--fog-dark-text-light, #9ca3af); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Profile badges in markdown content should align with text baseline */ |
|
|
|
|
:global(.markdown-content [data-nostr-profile]), |
|
|
|
|
:global(.markdown-content .profile-badge) { |
|
|
|
|
vertical-align: middle; |
|
|
|
|
display: inline-flex; |
|
|
|
|
align-items: center; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
|