diff --git a/src/hooks/useMarkdownToHTML.ts b/src/hooks/useMarkdownToHTML.ts index b9e9d0d0..995bb31c 100644 --- a/src/hooks/useMarkdownToHTML.ts +++ b/src/hooks/useMarkdownToHTML.ts @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react' +import React, { useState, useEffect, useRef, useMemo } from 'react' import { RelayPool } from 'applesauce-relay' import { extractNaddrUris, replaceNostrUrisInMarkdownWithProfileLabels } from '../utils/nostrUriResolver' import { fetchArticleTitles } from '../services/articleTitleResolver' @@ -23,6 +23,35 @@ export const useMarkdownToHTML = ( // Resolve profile labels progressively as profiles load const { labels: profileLabels, loading: profileLoading } = useProfileLabels(markdown || '', relayPool) + + // Create stable dependencies based on Map contents, not Map objects + // This prevents unnecessary reprocessing when Maps are recreated with same content + const profileLabelsKey = useMemo(() => { + return Array.from(profileLabels.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join('|') + }, [profileLabels]) + + const profileLoadingKey = useMemo(() => { + return Array.from(profileLoading.entries()) + .filter(([, loading]) => loading) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([k]) => k) + .join('|') + }, [profileLoading]) + + const articleTitlesKey = useMemo(() => { + return Array.from(articleTitles.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join('|') + }, [articleTitles]) + + // Keep refs to latest Maps for processing without causing re-renders + const profileLabelsRef = useRef(profileLabels) + const profileLoadingRef = useRef(profileLoading) + const articleTitlesRef = useRef(articleTitles) + + useEffect(() => { + profileLabelsRef.current = profileLabels + profileLoadingRef.current = profileLoading + articleTitlesRef.current = articleTitles + }, [profileLabels, profileLoading, articleTitles]) // Fetch article titles useEffect(() => { @@ -54,15 +83,27 @@ export const useMarkdownToHTML = ( return () => { isCancelled = true } }, [markdown, relayPool]) - // Process markdown with progressive profile labels and article titles + // Track previous markdown and processed state to detect actual content changes + const previousMarkdownRef = useRef(markdown) + const processedMarkdownRef = useRef(processedMarkdown) + useEffect(() => { - console.log(`[profile-loading-debug][markdown-to-html] Processing markdown, profileLabels=${profileLabels.size}, profileLoading=${profileLoading.size}, articleTitles=${articleTitles.size}`) - console.log(`[profile-loading-debug][markdown-to-html] Clearing rendered HTML and processed markdown`) - // Always clear previous render immediately to avoid showing stale content while processing - setRenderedHtml('') - setProcessedMarkdown('') + processedMarkdownRef.current = processedMarkdown + }, [processedMarkdown]) + + // Process markdown with progressive profile labels and article titles + // Use stable string keys instead of Map objects to prevent excessive reprocessing + useEffect(() => { + const labelsSize = profileLabelsRef.current.size + const loadingSize = profileLoadingRef.current.size + const titlesSize = articleTitlesRef.current.size + console.log(`[profile-loading-debug][markdown-to-html] Processing markdown, profileLabels=${labelsSize}, profileLoading=${loadingSize}, articleTitles=${titlesSize}`) if (!markdown) { + setRenderedHtml('') + setProcessedMarkdown('') + previousMarkdownRef.current = markdown + processedMarkdownRef.current = '' return } @@ -71,21 +112,27 @@ export const useMarkdownToHTML = ( const processMarkdown = () => { try { // Replace nostr URIs with profile labels (progressive) and article titles + // Use refs to get latest values without causing dependency changes const processed = replaceNostrUrisInMarkdownWithProfileLabels( markdown, - profileLabels, - articleTitles, - profileLoading + profileLabelsRef.current, + articleTitlesRef.current, + profileLoadingRef.current ) if (isCancelled) return - console.log(`[profile-loading-debug][markdown-to-html] Processed markdown, loading states:`, Array.from(profileLoading.entries()).filter(([, l]) => l).map(([e]) => e.slice(0, 16) + '...')) + const loadingStates = Array.from(profileLoadingRef.current.entries()) + .filter(([, l]) => l) + .map(([e]) => e.slice(0, 16) + '...') + console.log(`[profile-loading-debug][markdown-to-html] Processed markdown, loading states:`, loadingStates) setProcessedMarkdown(processed) + processedMarkdownRef.current = processed } catch (error) { console.error(`[markdown-to-html] Error processing markdown:`, error) if (!isCancelled) { setProcessedMarkdown(markdown) // Fallback to original + processedMarkdownRef.current = markdown } } @@ -101,12 +148,24 @@ export const useMarkdownToHTML = ( return () => cancelAnimationFrame(rafId) } + // Only clear previous content if this is the first processing or markdown changed + // For profile updates, just reprocess without clearing to avoid flicker + const isMarkdownChange = previousMarkdownRef.current !== markdown + previousMarkdownRef.current = markdown + + if (isMarkdownChange || !processedMarkdownRef.current) { + console.log(`[profile-loading-debug][markdown-to-html] Clearing rendered HTML and processed markdown (markdown changed: ${isMarkdownChange})`) + setRenderedHtml('') + setProcessedMarkdown('') + processedMarkdownRef.current = '' + } + processMarkdown() return () => { isCancelled = true } - }, [markdown, profileLabels, profileLoading, articleTitles]) + }, [markdown, profileLabelsKey, profileLoadingKey, articleTitlesKey]) return { renderedHtml, previewRef, processedMarkdown } }