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 @@ @@ -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 {
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 @@ @@ -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();
});
</script>
@ -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>

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

@ -2,6 +2,7 @@ @@ -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( @@ -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( @@ -40,18 +52,26 @@ export function mountComponent(
return {
update(newProps: Record<string, any>) {
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);
}

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

@ -5,9 +5,10 @@ @@ -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<string | null>(null);
@ -20,8 +21,11 @@ @@ -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 @@ @@ -87,26 +91,28 @@
</script>
<a href="/profile/{pubkey}" class="profile-badge inline-flex items-center gap-2 min-w-0 max-w-full">
{#if profile?.picture && !imageError}
<img
src={profile.picture}
alt={profile.name || pubkey}
class="profile-picture w-6 h-6 rounded flex-shrink-0"
onerror={() => {
imageError = true;
}}
/>
{:else}
<div
class="profile-placeholder w-6 h-6 rounded flex-shrink-0 flex items-center justify-center text-xs font-semibold"
style="background: {avatarColor}; color: white; filter: grayscale(100%) sepia(10%) hue-rotate(200deg) saturate(30%);"
title={pubkey}
>
{avatarInitials}
</div>
{#if !inline}
{#if profile?.picture && !imageError}
<img
src={profile.picture}
alt={profile.name || pubkey}
class="profile-picture w-6 h-6 rounded flex-shrink-0"
onerror={() => {
imageError = true;
}}
/>
{:else}
<div
class="profile-placeholder w-6 h-6 rounded flex-shrink-0 flex items-center justify-center text-xs font-semibold"
style="background: {avatarColor}; color: white; filter: grayscale(100%) sepia(10%) hue-rotate(200deg) saturate(30%);"
title={pubkey}
>
{avatarInitials}
</div>
{/if}
{/if}
<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>
{/if}
</a>

Loading…
Cancel
Save