mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +01:00
fix: reuse Explore article events to load articles immediately
- Add articleCoordinate and eventId to BlogPostCard navigation state - Update useArticleLoader to check navigation state first before cache/EventStore - Hydrate article content immediately from eventStore when coming from Explore - Preserve existing cache/EventStore paths for deep links - Add background query to check for newer replaceable versions without blocking UI - Guard updates with requestId to prevent race conditions This fixes the issue where articles opened from Explore would hang on loading skeleton when queryEvents never completes. Now articles load instantly by reusing the full event that Explore already fetched and cached.
This commit is contained in:
@@ -53,6 +53,10 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level, readingP
|
|||||||
// Reading progress display
|
// Reading progress display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build article coordinate for navigation state (kind:pubkey:dTag)
|
||||||
|
const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] || ''
|
||||||
|
const articleCoordinate = dTag ? `${post.event.kind}:${post.author}:${dTag}` : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={href}
|
to={href}
|
||||||
@@ -62,7 +66,9 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level, readingP
|
|||||||
image: post.image,
|
image: post.image,
|
||||||
summary: post.summary,
|
summary: post.summary,
|
||||||
published: post.published
|
published: post.published
|
||||||
}
|
},
|
||||||
|
articleCoordinate,
|
||||||
|
eventId: post.event.id
|
||||||
}}
|
}}
|
||||||
className={`blog-post-card ${level ? `level-${level}` : ''}`}
|
className={`blog-post-card ${level ? `level-${level}` : ''}`}
|
||||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ interface PreviewData {
|
|||||||
published?: number
|
published?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NavigationState {
|
||||||
|
previewData?: PreviewData
|
||||||
|
articleCoordinate?: string
|
||||||
|
eventId?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface UseArticleLoaderProps {
|
interface UseArticleLoaderProps {
|
||||||
naddr: string | undefined
|
naddr: string | undefined
|
||||||
relayPool: RelayPool | null
|
relayPool: RelayPool | null
|
||||||
@@ -63,8 +69,11 @@ export function useArticleLoader({
|
|||||||
// Track in-flight request to prevent stale updates from previous naddr
|
// Track in-flight request to prevent stale updates from previous naddr
|
||||||
const currentRequestIdRef = useRef(0)
|
const currentRequestIdRef = useRef(0)
|
||||||
|
|
||||||
// Extract preview data from navigation state (from blog post cards)
|
// Extract navigation state (from blog post cards)
|
||||||
const previewData = (location.state as { previewData?: PreviewData })?.previewData
|
const navState = (location.state as NavigationState | null) || {}
|
||||||
|
const previewData = navState.previewData
|
||||||
|
const navArticleCoordinate = navState.articleCoordinate
|
||||||
|
const navEventId = navState.eventId
|
||||||
|
|
||||||
// Track the current article title for document title
|
// Track the current article title for document title
|
||||||
const [currentTitle, setCurrentTitle] = useState<string | undefined>()
|
const [currentTitle, setCurrentTitle] = useState<string | undefined>()
|
||||||
@@ -83,6 +92,179 @@ export function useArticleLoader({
|
|||||||
// This ensures images from previous articles don't flash briefly
|
// This ensures images from previous articles don't flash briefly
|
||||||
setReaderContent(undefined)
|
setReaderContent(undefined)
|
||||||
|
|
||||||
|
// FIRST: Check navigation state for article coordinate/eventId (from Explore)
|
||||||
|
// This allows immediate hydration when coming from Explore without refetching
|
||||||
|
let foundInNavState = false
|
||||||
|
if (eventStore && (navArticleCoordinate || navEventId)) {
|
||||||
|
try {
|
||||||
|
let storedEvent: NostrEvent | undefined
|
||||||
|
|
||||||
|
// Try coordinate first (most reliable for replaceable events)
|
||||||
|
if (navArticleCoordinate) {
|
||||||
|
storedEvent = eventStore.getEvent?.(navArticleCoordinate) as NostrEvent | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to eventId if coordinate lookup failed
|
||||||
|
if (!storedEvent && navEventId) {
|
||||||
|
// Note: eventStore.getEvent might not support eventId lookup directly
|
||||||
|
// We'll decode naddr to get coordinate as fallback
|
||||||
|
try {
|
||||||
|
const decoded = nip19.decode(naddr)
|
||||||
|
if (decoded.type === 'naddr') {
|
||||||
|
const pointer = decoded.data as AddressPointer
|
||||||
|
const coordinate = `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`
|
||||||
|
storedEvent = eventStore.getEvent?.(coordinate) as NostrEvent | undefined
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore decode errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedEvent) {
|
||||||
|
foundInNavState = true
|
||||||
|
const title = Helpers.getArticleTitle(storedEvent) || previewData?.title || 'Untitled Article'
|
||||||
|
setCurrentTitle(title)
|
||||||
|
const image = Helpers.getArticleImage(storedEvent) || previewData?.image
|
||||||
|
const summary = Helpers.getArticleSummary(storedEvent) || previewData?.summary
|
||||||
|
const published = Helpers.getArticlePublished(storedEvent) || previewData?.published
|
||||||
|
setReaderContent({
|
||||||
|
title,
|
||||||
|
markdown: storedEvent.content,
|
||||||
|
image,
|
||||||
|
summary,
|
||||||
|
published,
|
||||||
|
url: `nostr:${naddr}`
|
||||||
|
})
|
||||||
|
const dTag = storedEvent.tags.find(t => t[0] === 'd')?.[1] || ''
|
||||||
|
const articleCoordinate = `${storedEvent.kind}:${storedEvent.pubkey}:${dTag}`
|
||||||
|
setCurrentArticleCoordinate(articleCoordinate)
|
||||||
|
setCurrentArticleEventId(storedEvent.id)
|
||||||
|
setCurrentArticle?.(storedEvent)
|
||||||
|
setReaderLoading(false)
|
||||||
|
setSelectedUrl(`nostr:${naddr}`)
|
||||||
|
setIsCollapsed(true)
|
||||||
|
|
||||||
|
// Preload image if available
|
||||||
|
if (image) {
|
||||||
|
preloadImage(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch highlights in background if relayPool is available
|
||||||
|
if (relayPool) {
|
||||||
|
const coord = dTag ? `${storedEvent.kind}:${storedEvent.pubkey}:${dTag}` : undefined
|
||||||
|
const eventId = storedEvent.id
|
||||||
|
|
||||||
|
if (coord && eventId) {
|
||||||
|
setHighlightsLoading(true)
|
||||||
|
fetchHighlightsForArticle(
|
||||||
|
relayPool,
|
||||||
|
coord,
|
||||||
|
eventId,
|
||||||
|
(highlight) => {
|
||||||
|
if (!mountedRef.current) return
|
||||||
|
setHighlights((prev: Highlight[]) => {
|
||||||
|
if (prev.some((h: Highlight) => h.id === highlight.id)) return prev
|
||||||
|
const next = [highlight, ...prev]
|
||||||
|
return next.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings,
|
||||||
|
false,
|
||||||
|
eventStore || undefined
|
||||||
|
).then(() => {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setHighlightsLoading(false)
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setHighlightsLoading(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start background query to check for newer replaceable version
|
||||||
|
// but don't block UI - we already have content
|
||||||
|
if (relayPool) {
|
||||||
|
const backgroundRequestId = ++currentRequestIdRef.current
|
||||||
|
const originalCreatedAt = storedEvent.created_at
|
||||||
|
|
||||||
|
// Fire and forget background fetch
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const decoded = nip19.decode(naddr)
|
||||||
|
if (decoded.type !== 'naddr') return
|
||||||
|
const pointer = decoded.data as AddressPointer
|
||||||
|
const filter = {
|
||||||
|
kinds: [pointer.kind],
|
||||||
|
authors: [pointer.pubkey],
|
||||||
|
'#d': [pointer.identifier]
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await queryEvents(relayPool, filter, {
|
||||||
|
onEvent: (evt) => {
|
||||||
|
if (!mountedRef.current || currentRequestIdRef.current !== backgroundRequestId) return
|
||||||
|
|
||||||
|
// Store in event store
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
eventStore?.add?.(evt as unknown as any)
|
||||||
|
} catch {
|
||||||
|
// Ignore store errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if this is a newer version than what we loaded
|
||||||
|
if (evt.created_at > originalCreatedAt) {
|
||||||
|
const title = Helpers.getArticleTitle(evt) || 'Untitled Article'
|
||||||
|
const image = Helpers.getArticleImage(evt)
|
||||||
|
const summary = Helpers.getArticleSummary(evt)
|
||||||
|
const published = Helpers.getArticlePublished(evt)
|
||||||
|
|
||||||
|
setCurrentTitle(title)
|
||||||
|
setReaderContent({
|
||||||
|
title,
|
||||||
|
markdown: evt.content,
|
||||||
|
image,
|
||||||
|
summary,
|
||||||
|
published,
|
||||||
|
url: `nostr:${naddr}`
|
||||||
|
})
|
||||||
|
const dTag = evt.tags.find(t => t[0] === 'd')?.[1] || ''
|
||||||
|
const articleCoordinate = `${evt.kind}:${evt.pubkey}:${dTag}`
|
||||||
|
setCurrentArticleCoordinate(articleCoordinate)
|
||||||
|
setCurrentArticleEventId(evt.id)
|
||||||
|
setCurrentArticle?.(evt)
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
const articleContent = {
|
||||||
|
title,
|
||||||
|
markdown: evt.content,
|
||||||
|
image,
|
||||||
|
summary,
|
||||||
|
published,
|
||||||
|
author: evt.pubkey,
|
||||||
|
event: evt
|
||||||
|
}
|
||||||
|
saveToCache(naddr, articleContent, settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
// Silently ignore background fetch errors - we already have content
|
||||||
|
console.warn('[article-loader] Background fetch failed:', err)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return early - we have content from navigation state
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// If navigation state lookup fails, fall through to cache/EventStore
|
||||||
|
console.warn('[article-loader] Navigation state lookup failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Synchronously check cache sources BEFORE checking relayPool
|
// Synchronously check cache sources BEFORE checking relayPool
|
||||||
// This prevents showing loading skeletons when content is immediately available
|
// This prevents showing loading skeletons when content is immediately available
|
||||||
// and fixes the race condition where relayPool isn't ready yet
|
// and fixes the race condition where relayPool isn't ready yet
|
||||||
@@ -173,7 +355,7 @@ export function useArticleLoader({
|
|||||||
|
|
||||||
// Check EventStore synchronously (also doesn't need relayPool)
|
// Check EventStore synchronously (also doesn't need relayPool)
|
||||||
let foundInEventStore = false
|
let foundInEventStore = false
|
||||||
if (eventStore && !foundInCache) {
|
if (eventStore && !foundInCache && !foundInNavState) {
|
||||||
try {
|
try {
|
||||||
// Decode naddr to get the coordinate
|
// Decode naddr to get the coordinate
|
||||||
const decoded = nip19.decode(naddr)
|
const decoded = nip19.decode(naddr)
|
||||||
@@ -251,7 +433,7 @@ export function useArticleLoader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only return early if we have no content AND no relayPool to fetch from
|
// Only return early if we have no content AND no relayPool to fetch from
|
||||||
if (!relayPool && !foundInCache && !foundInEventStore) {
|
if (!relayPool && !foundInCache && !foundInEventStore && !foundInNavState) {
|
||||||
setReaderLoading(true)
|
setReaderLoading(true)
|
||||||
setReaderContent(undefined)
|
setReaderContent(undefined)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user