Browse Source

bug-fix

master
Silberengel 1 month ago
parent
commit
e7d831c73a
  1. 123
      src/lib/components/content/MarkdownRenderer.svelte
  2. 54
      src/lib/components/content/mount-component-action.ts
  3. 48
      src/lib/components/layout/ProfileBadge.svelte

123
src/lib/components/content/MarkdownRenderer.svelte

@ -17,26 +17,25 @@
function getPubkeyFromNIP21(parsed: ReturnType<typeof parseNIP21>): string | null { function getPubkeyFromNIP21(parsed: ReturnType<typeof parseNIP21>): string | null {
if (!parsed) return null; if (!parsed) return null;
if (parsed.type === 'npub') { try {
// npub decodes directly to pubkey // parsed.data is the bech32 string (e.g., "npub1..." or "nprofile1...")
try { // We need to decode it to get the actual pubkey
const decoded = nip19.decode(parsed.data); const decoded = nip19.decode(parsed.data);
if (parsed.type === 'npub') {
// npub decodes directly to pubkey (hex string)
if (decoded.type === 'npub') { if (decoded.type === 'npub') {
return String(decoded.data); return String(decoded.data);
} }
} catch { } else if (parsed.type === 'nprofile') {
return null; // nprofile decodes to object with pubkey property
}
} 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) { if (decoded.type === 'nprofile' && decoded.data && typeof decoded.data === 'object' && 'pubkey' in decoded.data) {
return String(decoded.data.pubkey); return String(decoded.data.pubkey);
} }
} catch {
return null;
} }
} catch (error) {
console.error('Error decoding NIP-21 URI:', error, parsed);
return null;
} }
return null; return null;
@ -173,27 +172,83 @@
const renderedHtml = $derived(renderMarkdown(content)); const renderedHtml = $derived(renderMarkdown(content));
// Mount ProfileBadge components after rendering // 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(() => { $effect(() => {
if (!containerRef || !renderedHtml) return; if (!containerRef || !renderedHtml) return;
// Use a small delay to ensure DOM is updated // Use requestAnimationFrame + setTimeout to ensure DOM is ready
const timeoutId = setTimeout(() => { const frameId = requestAnimationFrame(() => {
if (!containerRef) return; const timeoutId = setTimeout(() => {
mountProfileBadges();
// Find all profile placeholders and mount ProfileBadge components }, 150);
const placeholders = containerRef.querySelectorAll('[data-nostr-profile]');
return () => clearTimeout(timeoutId);
placeholders.forEach((placeholder) => { });
const pubkey = placeholder.getAttribute('data-pubkey');
if (pubkey && !placeholder.hasAttribute('data-mounted')) { return () => cancelAnimationFrame(frameId);
// Mark as mounted to avoid double-mounting });
placeholder.setAttribute('data-mounted', 'true');
mountComponent(placeholder as HTMLElement, ProfileBadge as any, { pubkey });
}
});
}, 0);
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();
}); });
</script> </script>
@ -286,4 +341,12 @@
border-color: var(--fog-dark-border, #374151); border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text-light, #9ca3af); 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> </style>

54
src/lib/components/content/mount-component-action.ts

@ -2,6 +2,7 @@
* Svelte action to mount a Svelte component into a DOM element * Svelte action to mount a Svelte component into a DOM element
*/ */
import type { ComponentType, Snippet } from 'svelte'; import type { ComponentType, Snippet } from 'svelte';
import { mount, unmount } from 'svelte';
export function mountComponent( export function mountComponent(
node: HTMLElement, node: HTMLElement,
@ -12,24 +13,35 @@ export function mountComponent(
// Mount the component // Mount the component
if (component && typeof component === 'function') { if (component && typeof component === 'function') {
// Using Svelte 4 component API (enabled via compatibility mode in svelte.config.js)
try { try {
// Clear the node first to ensure clean mounting // Clear the node first to ensure clean mounting
node.innerHTML = ''; node.innerHTML = '';
// Create a new instance // Try using Svelte 5's mount function first
// In Svelte 5 with compatibility mode, components are instantiated with the Svelte 4 API try {
instance = new (component as any)({ instance = mount(component, {
target: node, target: node,
props, props
// Ensure the component is hydrated and rendered });
hydrate: false, } catch (e) {
intro: false // 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 // Verify the component was mounted
if (!instance) { if (!instance) {
console.warn('[mountComponent] Component instance not created', { component, props }); 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) { } catch (e) {
console.error('[mountComponent] Failed to mount component:', e, { component, props, node }); console.error('[mountComponent] Failed to mount component:', e, { component, props, node });
@ -40,18 +52,26 @@ export function mountComponent(
return { return {
update(newProps: Record<string, any>) { update(newProps: Record<string, any>) {
if (instance && typeof instance.$set === 'function') { if (instance) {
try { // Try Svelte 5 update first
instance.$set(newProps); if (typeof instance.$set === 'function') {
} catch (e) { try {
console.error('[mountComponent] Failed to update component:', e); instance.$set(newProps);
} catch (e) {
console.error('[mountComponent] Failed to update component:', e);
}
} }
} }
}, },
destroy() { destroy() {
if (instance && typeof instance.$destroy === 'function') { if (instance) {
// Try Svelte 5 unmount first
try { try {
instance.$destroy(); if (typeof unmount === 'function') {
unmount(instance);
} else if (typeof instance.$destroy === 'function') {
instance.$destroy();
}
} catch (e) { } catch (e) {
console.error('[mountComponent] Failed to destroy component:', e); console.error('[mountComponent] Failed to destroy component:', e);
} }

48
src/lib/components/layout/ProfileBadge.svelte

@ -5,9 +5,10 @@
interface Props { interface Props {
pubkey: string; 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 profile = $state<{ name?: string; picture?: string } | null>(null);
let status = $state<string | null>(null); let status = $state<string | null>(null);
@ -20,8 +21,11 @@
imageError = false; // Reset image error when pubkey changes imageError = false; // Reset image error when pubkey changes
// Load immediately - no debounce // Load immediately - no debounce
loadProfile(); loadProfile();
loadStatus(); // Only load status and activity if not inline
updateActivityStatus(); if (!inline) {
loadStatus();
updateActivityStatus();
}
} }
}); });
@ -87,26 +91,28 @@
</script> </script>
<a href="/profile/{pubkey}" class="profile-badge inline-flex items-center gap-2 min-w-0 max-w-full"> <a href="/profile/{pubkey}" class="profile-badge inline-flex items-center gap-2 min-w-0 max-w-full">
{#if profile?.picture && !imageError} {#if !inline}
<img {#if profile?.picture && !imageError}
src={profile.picture} <img
alt={profile.name || pubkey} src={profile.picture}
class="profile-picture w-6 h-6 rounded flex-shrink-0" alt={profile.name || pubkey}
onerror={() => { class="profile-picture w-6 h-6 rounded flex-shrink-0"
imageError = true; onerror={() => {
}} imageError = true;
/> }}
{:else} />
<div {:else}
class="profile-placeholder w-6 h-6 rounded flex-shrink-0 flex items-center justify-center text-xs font-semibold" <div
style="background: {avatarColor}; color: white; filter: grayscale(100%) sepia(10%) hue-rotate(200deg) saturate(30%);" class="profile-placeholder w-6 h-6 rounded flex-shrink-0 flex items-center justify-center text-xs font-semibold"
title={pubkey} style="background: {avatarColor}; color: white; filter: grayscale(100%) sepia(10%) hue-rotate(200deg) saturate(30%);"
> title={pubkey}
{avatarInitials} >
</div> {avatarInitials}
</div>
{/if}
{/if} {/if}
<span class="truncate min-w-0">{profile?.name || shortenedNpub}</span> <span class="truncate min-w-0">{profile?.name || shortenedNpub}</span>
{#if status} {#if !inline && status}
<span class="status-text text-sm text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0 whitespace-nowrap">({status})</span> <span class="status-text text-sm text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0 whitespace-nowrap">({status})</span>
{/if} {/if}
</a> </a>

Loading…
Cancel
Save