|
|
|
@ -4,6 +4,7 @@ |
|
|
|
import { findNIP21Links, parseNIP21 } from '../../services/nostr/nip21-parser.js'; |
|
|
|
import { findNIP21Links, parseNIP21 } from '../../services/nostr/nip21-parser.js'; |
|
|
|
import { nip19 } from 'nostr-tools'; |
|
|
|
import { nip19 } from 'nostr-tools'; |
|
|
|
import ProfileBadge from '../layout/ProfileBadge.svelte'; |
|
|
|
import ProfileBadge from '../layout/ProfileBadge.svelte'; |
|
|
|
|
|
|
|
import EmbeddedEvent from './EmbeddedEvent.svelte'; |
|
|
|
import { mountComponent } from './mount-component-action.js'; |
|
|
|
import { mountComponent } from './mount-component-action.js'; |
|
|
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
interface Props { |
|
|
|
@ -41,6 +42,19 @@ |
|
|
|
return null; |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Extract event identifier from note, nevent, or naddr |
|
|
|
|
|
|
|
// Returns the bech32 string (note1..., nevent1..., naddr1...) which EmbeddedEvent can decode |
|
|
|
|
|
|
|
function getEventIdFromNIP21(parsed: ReturnType<typeof parseNIP21>): string | null { |
|
|
|
|
|
|
|
if (!parsed) return null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (parsed.type === 'note' || parsed.type === 'nevent' || parsed.type === 'naddr') { |
|
|
|
|
|
|
|
// Return the bech32 string - EmbeddedEvent will decode it |
|
|
|
|
|
|
|
return parsed.data; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Escape HTML to prevent XSS |
|
|
|
// Escape HTML to prevent XSS |
|
|
|
function escapeHtml(text: string): string { |
|
|
|
function escapeHtml(text: string): string { |
|
|
|
return text |
|
|
|
return text |
|
|
|
@ -111,14 +125,35 @@ |
|
|
|
// First, convert plain media URLs (images, videos, audio) to HTML tags |
|
|
|
// First, convert plain media URLs (images, videos, audio) to HTML tags |
|
|
|
let processed = convertMediaUrls(text); |
|
|
|
let processed = convertMediaUrls(text); |
|
|
|
|
|
|
|
|
|
|
|
// Find all NIP-21 links (nostr:npub, nostr:nprofile, etc.) |
|
|
|
// Find all NIP-21 links (nostr:npub, nostr:nprofile, nostr:nevent, etc.) |
|
|
|
const links = findNIP21Links(processed); |
|
|
|
const links = findNIP21Links(processed); |
|
|
|
|
|
|
|
|
|
|
|
// Only process npub and nprofile for profile badges |
|
|
|
// Separate into profile links and event links |
|
|
|
const profileLinks = links.filter(link => |
|
|
|
const profileLinks = links.filter(link => |
|
|
|
link.parsed.type === 'npub' || link.parsed.type === 'nprofile' |
|
|
|
link.parsed.type === 'npub' || link.parsed.type === 'nprofile' |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const eventLinks = links.filter(link => |
|
|
|
|
|
|
|
link.parsed.type === 'note' || link.parsed.type === 'nevent' || link.parsed.type === 'naddr' |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Replace event links with HTML div elements (for block-level display) |
|
|
|
|
|
|
|
// Process from end to start to preserve indices |
|
|
|
|
|
|
|
for (let i = eventLinks.length - 1; i >= 0; i--) { |
|
|
|
|
|
|
|
const link = eventLinks[i]; |
|
|
|
|
|
|
|
const eventId = getEventIdFromNIP21(link.parsed); |
|
|
|
|
|
|
|
if (eventId) { |
|
|
|
|
|
|
|
// Escape event ID to prevent XSS |
|
|
|
|
|
|
|
const escapedEventId = escapeHtml(eventId); |
|
|
|
|
|
|
|
// Create a div element for embedded event cards (block-level) |
|
|
|
|
|
|
|
const div = `<div data-nostr-event data-event-id="${escapedEventId}"></div>`; |
|
|
|
|
|
|
|
processed = |
|
|
|
|
|
|
|
processed.slice(0, link.start) + |
|
|
|
|
|
|
|
div + |
|
|
|
|
|
|
|
processed.slice(link.end); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Replace profile links with HTML span elements that have data attributes |
|
|
|
// Replace profile links with HTML span elements that have data attributes |
|
|
|
// Process from end to start to preserve indices |
|
|
|
// Process from end to start to preserve indices |
|
|
|
for (let i = profileLinks.length - 1; i >= 0; i--) { |
|
|
|
for (let i = profileLinks.length - 1; i >= 0; i--) { |
|
|
|
@ -178,8 +213,7 @@ |
|
|
|
// Find all profile placeholders and mount ProfileBadge components |
|
|
|
// Find all profile placeholders and mount ProfileBadge components |
|
|
|
const placeholders = containerRef.querySelectorAll('[data-nostr-profile]:not([data-mounted])'); |
|
|
|
const placeholders = containerRef.querySelectorAll('[data-nostr-profile]:not([data-mounted])'); |
|
|
|
|
|
|
|
|
|
|
|
if (placeholders.length === 0) return; |
|
|
|
if (placeholders.length > 0) { |
|
|
|
|
|
|
|
|
|
|
|
console.debug(`Mounting ${placeholders.length} ProfileBadge components`); |
|
|
|
console.debug(`Mounting ${placeholders.length} ProfileBadge components`); |
|
|
|
|
|
|
|
|
|
|
|
placeholders.forEach((placeholder) => { |
|
|
|
placeholders.forEach((placeholder) => { |
|
|
|
@ -219,6 +253,43 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Mount EmbeddedEvent components after rendering |
|
|
|
|
|
|
|
function mountEmbeddedEvents() { |
|
|
|
|
|
|
|
if (!containerRef) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Find all event placeholders and mount EmbeddedEvent components |
|
|
|
|
|
|
|
const placeholders = containerRef.querySelectorAll('[data-nostr-event]:not([data-mounted])'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (placeholders.length > 0) { |
|
|
|
|
|
|
|
console.debug(`Mounting ${placeholders.length} EmbeddedEvent components`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
placeholders.forEach((placeholder) => { |
|
|
|
|
|
|
|
const eventId = placeholder.getAttribute('data-event-id'); |
|
|
|
|
|
|
|
if (eventId) { |
|
|
|
|
|
|
|
placeholder.setAttribute('data-mounted', 'true'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
// Clear and mount component |
|
|
|
|
|
|
|
placeholder.innerHTML = ''; |
|
|
|
|
|
|
|
// Mount EmbeddedEvent component - it will decode and fetch the event |
|
|
|
|
|
|
|
const instance = mountComponent(placeholder as HTMLElement, EmbeddedEvent as any, { eventId }); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!instance) { |
|
|
|
|
|
|
|
console.warn('EmbeddedEvent mount returned null', { eventId }); |
|
|
|
|
|
|
|
// Fallback: show the event ID |
|
|
|
|
|
|
|
placeholder.textContent = eventId.slice(0, 20) + '...'; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error('Error mounting EmbeddedEvent:', error, { eventId }); |
|
|
|
|
|
|
|
// Show fallback |
|
|
|
|
|
|
|
placeholder.textContent = eventId.slice(0, 20) + '...'; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$effect(() => { |
|
|
|
$effect(() => { |
|
|
|
if (!containerRef || !renderedHtml) return; |
|
|
|
if (!containerRef || !renderedHtml) return; |
|
|
|
@ -227,6 +298,7 @@ |
|
|
|
const frameId = requestAnimationFrame(() => { |
|
|
|
const frameId = requestAnimationFrame(() => { |
|
|
|
const timeoutId = setTimeout(() => { |
|
|
|
const timeoutId = setTimeout(() => { |
|
|
|
mountProfileBadges(); |
|
|
|
mountProfileBadges(); |
|
|
|
|
|
|
|
mountEmbeddedEvents(); |
|
|
|
}, 150); |
|
|
|
}, 150); |
|
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timeoutId); |
|
|
|
return () => clearTimeout(timeoutId); |
|
|
|
@ -241,6 +313,7 @@ |
|
|
|
|
|
|
|
|
|
|
|
const observer = new MutationObserver(() => { |
|
|
|
const observer = new MutationObserver(() => { |
|
|
|
mountProfileBadges(); |
|
|
|
mountProfileBadges(); |
|
|
|
|
|
|
|
mountEmbeddedEvents(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
observer.observe(containerRef, { |
|
|
|
observer.observe(containerRef, { |
|
|
|
@ -349,4 +422,10 @@ |
|
|
|
display: inline-flex; |
|
|
|
display: inline-flex; |
|
|
|
align-items: center; |
|
|
|
align-items: center; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Embedded events should be block-level */ |
|
|
|
|
|
|
|
:global(.markdown-content [data-nostr-event]) { |
|
|
|
|
|
|
|
display: block; |
|
|
|
|
|
|
|
margin: 1rem 0; |
|
|
|
|
|
|
|
} |
|
|
|
</style> |
|
|
|
</style> |
|
|
|
|