mirror of
https://github.com/dergigi/boris.git
synced 2026-01-17 22:04:32 +01:00
- Replace useMountedState custom hook with inline useRef approach - Set mountedRef.current = true at start of each effect run - Ensures proper reset when navigating between articles - Simpler and more reliable than custom hook approach
177 lines
5.8 KiB
TypeScript
177 lines
5.8 KiB
TypeScript
import { useEffect, useRef, useMemo } from 'react'
|
|
import { RelayPool } from 'applesauce-relay'
|
|
import { IEventStore } from 'applesauce-core'
|
|
import { fetchReadableContent, ReadableContent } from '../services/readerService'
|
|
import { fetchHighlightsForUrl } from '../services/highlightService'
|
|
import { Highlight } from '../types/highlights'
|
|
import { useStoreTimeline } from './useStoreTimeline'
|
|
import { eventToHighlight } from '../services/highlightEventProcessor'
|
|
import { KINDS } from '../config/kinds'
|
|
|
|
// Helper to extract filename from URL
|
|
function getFilenameFromUrl(url: string): string {
|
|
try {
|
|
const urlObj = new URL(url)
|
|
const pathname = urlObj.pathname
|
|
const filename = pathname.substring(pathname.lastIndexOf('/') + 1)
|
|
// Decode URI component to handle special characters
|
|
return decodeURIComponent(filename) || url
|
|
} catch {
|
|
return url
|
|
}
|
|
}
|
|
|
|
interface UseExternalUrlLoaderProps {
|
|
url: string | undefined
|
|
relayPool: RelayPool | null
|
|
eventStore?: IEventStore | null
|
|
setSelectedUrl: (url: string) => void
|
|
setReaderContent: (content: ReadableContent | undefined) => void
|
|
setReaderLoading: (loading: boolean) => void
|
|
setIsCollapsed: (collapsed: boolean) => void
|
|
setHighlights: (highlights: Highlight[] | ((prev: Highlight[]) => Highlight[])) => void
|
|
setHighlightsLoading: (loading: boolean) => void
|
|
setCurrentArticleCoordinate: (coord: string | undefined) => void
|
|
setCurrentArticleEventId: (id: string | undefined) => void
|
|
}
|
|
|
|
export function useExternalUrlLoader({
|
|
url,
|
|
relayPool,
|
|
eventStore,
|
|
setSelectedUrl,
|
|
setReaderContent,
|
|
setReaderLoading,
|
|
setIsCollapsed,
|
|
setHighlights,
|
|
setHighlightsLoading,
|
|
setCurrentArticleCoordinate,
|
|
setCurrentArticleEventId
|
|
}: UseExternalUrlLoaderProps) {
|
|
const mountedRef = useRef(true)
|
|
|
|
// Load cached URL-specific highlights from event store
|
|
const urlFilter = useMemo(() => {
|
|
if (!url) return null
|
|
return { kinds: [KINDS.Highlights], '#r': [url] }
|
|
}, [url])
|
|
|
|
const cachedUrlHighlights = useStoreTimeline(
|
|
eventStore || null,
|
|
urlFilter || { kinds: [KINDS.Highlights], limit: 0 },
|
|
eventToHighlight,
|
|
[url]
|
|
)
|
|
|
|
// Load content and start streaming highlights when URL changes
|
|
useEffect(() => {
|
|
mountedRef.current = true
|
|
|
|
if (!relayPool || !url) return
|
|
|
|
const loadExternalUrl = async () => {
|
|
if (!mountedRef.current) return
|
|
|
|
setReaderLoading(true)
|
|
setReaderContent(undefined)
|
|
setSelectedUrl(url)
|
|
setIsCollapsed(true)
|
|
setCurrentArticleCoordinate(undefined)
|
|
setCurrentArticleEventId(undefined)
|
|
|
|
try {
|
|
const content = await fetchReadableContent(url)
|
|
|
|
if (!mountedRef.current) return
|
|
|
|
setReaderContent(content)
|
|
setReaderLoading(false)
|
|
|
|
// Fetch highlights for this URL asynchronously
|
|
try {
|
|
if (!mountedRef.current) return
|
|
|
|
setHighlightsLoading(true)
|
|
|
|
// Seed with cached highlights first
|
|
if (cachedUrlHighlights.length > 0) {
|
|
setHighlights((prev) => {
|
|
const seen = new Set<string>(cachedUrlHighlights.map(h => h.id))
|
|
const localOnly = prev.filter(h => !seen.has(h.id))
|
|
const next = [...cachedUrlHighlights, ...localOnly]
|
|
return next.sort((a, b) => b.created_at - a.created_at)
|
|
})
|
|
} else {
|
|
setHighlights([])
|
|
}
|
|
|
|
if (typeof fetchHighlightsForUrl === 'function') {
|
|
const seen = new Set<string>()
|
|
cachedUrlHighlights.forEach(h => seen.add(h.id))
|
|
|
|
await fetchHighlightsForUrl(
|
|
relayPool,
|
|
url,
|
|
(highlight) => {
|
|
if (!mountedRef.current) return
|
|
|
|
if (seen.has(highlight.id)) return
|
|
seen.add(highlight.id)
|
|
setHighlights((prev) => {
|
|
if (prev.some(h => h.id === highlight.id)) return prev
|
|
const next = [highlight, ...prev]
|
|
return next.sort((a, b) => b.created_at - a.created_at)
|
|
})
|
|
},
|
|
undefined,
|
|
false,
|
|
eventStore || undefined
|
|
)
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch highlights:', err)
|
|
} finally {
|
|
if (mountedRef.current) {
|
|
setHighlightsLoading(false)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load external URL:', err)
|
|
if (mountedRef.current) {
|
|
const filename = getFilenameFromUrl(url)
|
|
setReaderContent({
|
|
title: filename,
|
|
html: `<p>Failed to load content: ${err instanceof Error ? err.message : 'Unknown error'}</p>`,
|
|
url
|
|
})
|
|
setReaderLoading(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
loadExternalUrl()
|
|
|
|
return () => {
|
|
mountedRef.current = false
|
|
}
|
|
// Intentionally excluding setter functions from dependencies to prevent race conditions
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [url, relayPool, eventStore, cachedUrlHighlights])
|
|
|
|
// Keep UI highlights synced with cached store updates without reloading content
|
|
useEffect(() => {
|
|
if (!url) return
|
|
if (cachedUrlHighlights.length === 0) return
|
|
setHighlights((prev) => {
|
|
const seen = new Set<string>(prev.map(h => h.id))
|
|
const additions = cachedUrlHighlights.filter(h => !seen.has(h.id))
|
|
if (additions.length === 0) return prev
|
|
const next = [...additions, ...prev]
|
|
return next.sort((a, b) => b.created_at - a.created_at)
|
|
})
|
|
// setHighlights is intentionally excluded from dependencies - it's stable
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [cachedUrlHighlights, url])
|
|
}
|
|
|