From e7d831c73a07cfad589d37ff53a7454fd5e4b29e Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 3 Feb 2026 19:25:08 +0100 Subject: [PATCH] bug-fix --- .../content/MarkdownRenderer.svelte | 123 +++++++++++++----- .../content/mount-component-action.ts | 54 +++++--- src/lib/components/layout/ProfileBadge.svelte | 48 ++++--- 3 files changed, 157 insertions(+), 68 deletions(-) diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte index da1aee9..8b82fda 100644 --- a/src/lib/components/content/MarkdownRenderer.svelte +++ b/src/lib/components/content/MarkdownRenderer.svelte @@ -17,26 +17,25 @@ function getPubkeyFromNIP21(parsed: ReturnType): string | null { if (!parsed) return null; - if (parsed.type === 'npub') { - // npub decodes directly to pubkey - try { - const decoded = nip19.decode(parsed.data); + 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); + } else if (parsed.type === 'nprofile') { + // nprofile decodes to object with pubkey property 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,27 +172,83 @@ const renderedHtml = $derived(renderMarkdown(content)); // Mount ProfileBadge components after rendering + function mountProfileBadges() { + if (!containerRef) return; + + // Find all profile placeholders and mount ProfileBadge components + 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 && /^[0-9a-f]{64}$/i.test(pubkey)) { + placeholder.setAttribute('data-mounted', 'true'); + + 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) + '...'; + } + }); + } + $effect(() => { if (!containerRef || !renderedHtml) return; - // Use a small delay to ensure DOM is updated - const timeoutId = setTimeout(() => { - if (!containerRef) return; - - // Find all profile placeholders and mount ProfileBadge components - const placeholders = containerRef.querySelectorAll('[data-nostr-profile]'); - - placeholders.forEach((placeholder) => { - const pubkey = placeholder.getAttribute('data-pubkey'); - if (pubkey && !placeholder.hasAttribute('data-mounted')) { - // Mark as mounted to avoid double-mounting - placeholder.setAttribute('data-mounted', 'true'); - mountComponent(placeholder as HTMLElement, ProfileBadge as any, { pubkey }); - } - }); - }, 0); + // Use requestAnimationFrame + setTimeout to ensure DOM is ready + const frameId = requestAnimationFrame(() => { + const timeoutId = setTimeout(() => { + mountProfileBadges(); + }, 150); + + return () => clearTimeout(timeoutId); + }); + + return () => cancelAnimationFrame(frameId); + }); - return () => clearTimeout(timeoutId); + // 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(); }); @@ -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; + } diff --git a/src/lib/components/content/mount-component-action.ts b/src/lib/components/content/mount-component-action.ts index 9433817..e2c142b 100644 --- a/src/lib/components/content/mount-component-action.ts +++ b/src/lib/components/content/mount-component-action.ts @@ -2,6 +2,7 @@ * Svelte action to mount a Svelte component into a DOM element */ import type { ComponentType, Snippet } from 'svelte'; +import { mount, unmount } from 'svelte'; export function mountComponent( node: HTMLElement, @@ -12,24 +13,35 @@ export function mountComponent( // Mount the component if (component && typeof component === 'function') { - // Using Svelte 4 component API (enabled via compatibility mode in svelte.config.js) try { // Clear the node first to ensure clean mounting node.innerHTML = ''; - // Create a new instance - // In Svelte 5 with compatibility mode, components are instantiated with the Svelte 4 API - instance = new (component as any)({ - target: node, - props, - // Ensure the component is hydrated and rendered - hydrate: false, - intro: false - }); + // Try using Svelte 5's mount function first + try { + instance = mount(component, { + target: node, + props + }); + } catch (e) { + // Fallback to Svelte 4 compatibility API + console.debug('[mountComponent] Svelte 5 mount failed, trying compatibility API:', e); + instance = new (component as any)({ + target: node, + props, + hydrate: false, + intro: false + }); + } // Verify the component was mounted if (!instance) { console.warn('[mountComponent] Component instance not created', { component, props }); + } else { + // Force a tick to ensure effects run + setTimeout(() => { + // Effects should have run by now + }, 0); } } catch (e) { console.error('[mountComponent] Failed to mount component:', e, { component, props, node }); @@ -40,18 +52,26 @@ export function mountComponent( return { update(newProps: Record) { - if (instance && typeof instance.$set === 'function') { - try { - instance.$set(newProps); - } catch (e) { - console.error('[mountComponent] Failed to update component:', e); + if (instance) { + // Try Svelte 5 update first + if (typeof instance.$set === 'function') { + try { + instance.$set(newProps); + } catch (e) { + console.error('[mountComponent] Failed to update component:', e); + } } } }, destroy() { - if (instance && typeof instance.$destroy === 'function') { + if (instance) { + // Try Svelte 5 unmount first try { - instance.$destroy(); + if (typeof unmount === 'function') { + unmount(instance); + } else if (typeof instance.$destroy === 'function') { + instance.$destroy(); + } } catch (e) { console.error('[mountComponent] Failed to destroy component:', e); } diff --git a/src/lib/components/layout/ProfileBadge.svelte b/src/lib/components/layout/ProfileBadge.svelte index 83b1dce..d5783b0 100644 --- a/src/lib/components/layout/ProfileBadge.svelte +++ b/src/lib/components/layout/ProfileBadge.svelte @@ -5,9 +5,10 @@ interface Props { pubkey: string; + inline?: boolean; // If true, show only handle/name (no picture, status, etc.) } - let { pubkey }: Props = $props(); + let { pubkey, inline = false }: Props = $props(); let profile = $state<{ name?: string; picture?: string } | null>(null); let status = $state(null); @@ -20,8 +21,11 @@ imageError = false; // Reset image error when pubkey changes // Load immediately - no debounce loadProfile(); - loadStatus(); - updateActivityStatus(); + // Only load status and activity if not inline + if (!inline) { + loadStatus(); + updateActivityStatus(); + } } }); @@ -87,26 +91,28 @@ - {#if profile?.picture && !imageError} - {profile.name { - imageError = true; - }} - /> - {:else} -
- {avatarInitials} -
+ {#if !inline} + {#if profile?.picture && !imageError} + {profile.name { + imageError = true; + }} + /> + {:else} +
+ {avatarInitials} +
+ {/if} {/if} {profile?.name || shortenedNpub} - {#if status} + {#if !inline && status} ({status}) {/if}