diff --git a/src/hooks/useMarkdownToHTML.ts b/src/hooks/useMarkdownToHTML.ts index 995bb31c..5d256e7e 100644 --- a/src/hooks/useMarkdownToHTML.ts +++ b/src/hooks/useMarkdownToHTML.ts @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, useMemo } from 'react' import { RelayPool } from 'applesauce-relay' -import { extractNaddrUris, replaceNostrUrisInMarkdownWithProfileLabels } from '../utils/nostrUriResolver' +import { extractNaddrUris, replaceNostrUrisInMarkdownWithProfileLabels, addLoadingClassToProfileLinks } from '../utils/nostrUriResolver' import { fetchArticleTitles } from '../services/articleTitleResolver' import { useProfileLabels } from './useProfileLabels' @@ -138,7 +138,9 @@ export const useMarkdownToHTML = ( const rafId = requestAnimationFrame(() => { if (previewRef.current && !isCancelled) { - const html = previewRef.current.innerHTML + let html = previewRef.current.innerHTML + // Post-process HTML to add loading class to profile links + html = addLoadingClassToProfileLinks(html, profileLoadingRef.current) setRenderedHtml(html) } else if (!isCancelled) { console.warn('⚠️ markdownPreviewRef.current is null') diff --git a/src/utils/nostrUriResolver.tsx b/src/utils/nostrUriResolver.tsx index 335f9c2c..8297a2a3 100644 --- a/src/utils/nostrUriResolver.tsx +++ b/src/utils/nostrUriResolver.tsx @@ -338,20 +338,15 @@ export function replaceNostrUrisInMarkdownWithProfileLabels( if (decoded.type === 'npub' || decoded.type === 'nprofile') { const pubkey = decoded.type === 'npub' ? decoded.data : decoded.data.pubkey - // Check loading state FIRST - show loading even if we have a fallback label - const isLoading = profileLoading.get(pubkey) - if (isLoading === true) { - const label = getNostrUriLabel(encoded) - console.log(`[profile-loading-debug][nostr-uri-resolve] ${pubkey.slice(0, 16)}... is LOADING, showing loading state`) - // Wrap in span with profile-loading class for CSS styling - return `[${label}](${link})` - } - // Check if we have a resolved profile name using pubkey as key if (profileLabels.has(pubkey)) { const displayName = profileLabels.get(pubkey)! return `[${displayName}](${link})` } + + // If no resolved label yet, use fallback (will show loading via post-processing) + const label = getNostrUriLabel(encoded) + return `[${label}](${link})` } } catch (error) { // Ignore decode errors, fall through to default label @@ -363,6 +358,51 @@ export function replaceNostrUrisInMarkdownWithProfileLabels( }) } +/** + * Post-process rendered HTML to add loading class to profile links that are still loading + * This is necessary because HTML inside markdown links doesn't render correctly + * @param html The rendered HTML string + * @param profileLoading Map of pubkey (hex) -> boolean indicating if profile is loading + * @returns HTML with profile-loading class added to loading profile links + */ +export function addLoadingClassToProfileLinks( + html: string, + profileLoading: Map +): string { + if (profileLoading.size === 0) return html + + // Find all tags with href starting with /p/ (profile links) + return html.replace(/]*?href="\/p\/([^"]+)"[^>]*?>/g, (match, npub) => { + try { + // Decode npub to get pubkey + const decoded = decode(npub) + if (decoded.type !== 'npub') return match + + const pubkey = decoded.data + + // Check if this profile is loading + if (profileLoading.get(pubkey) === true) { + // Add profile-loading class if not already present + if (!match.includes('profile-loading')) { + // Insert class before the closing > + const classMatch = /class="([^"]*)"/.exec(match) + if (classMatch) { + // Update existing class attribute + return match.replace(/class="([^"]*)"/, `class="$1 profile-loading"`) + } else { + // Add new class attribute + return match.replace(/(]*?)>/, '$1 class="profile-loading">') + } + } + } + } catch (error) { + // If decoding fails, just return the original match + } + + return match + }) +} + /** * Replace nostr: URIs in HTML with clickable links * This is used when processing HTML content directly