diff --git a/src/hooks/useArticleLoader.ts b/src/hooks/useArticleLoader.ts index 2b37f073..69e8cbbc 100644 --- a/src/hooks/useArticleLoader.ts +++ b/src/hooks/useArticleLoader.ts @@ -1,5 +1,10 @@ import { useEffect, useRef, Dispatch, SetStateAction } from 'react' import { RelayPool } from 'applesauce-relay' +import type { IEventStore } from 'applesauce-core' +import { nip19 } from 'nostr-tools' +import { AddressPointer } from 'nostr-tools/nip19' +import { Helpers } from 'applesauce-core' +import { queryEvents } from '../services/dataFetch' import { fetchArticleByNaddr } from '../services/articleService' import { fetchHighlightsForArticle } from '../services/highlightService' import { ReadableContent } from '../services/readerService' @@ -10,6 +15,7 @@ import { UserSettings } from '../services/settingsService' interface UseArticleLoaderProps { naddr: string | undefined relayPool: RelayPool | null + eventStore?: IEventStore | null setSelectedUrl: (url: string) => void setReaderContent: (content: ReadableContent | undefined) => void setReaderLoading: (loading: boolean) => void @@ -25,6 +31,7 @@ interface UseArticleLoaderProps { export function useArticleLoader({ naddr, relayPool, + eventStore, setSelectedUrl, setReaderContent, setReaderLoading, @@ -60,52 +67,128 @@ export function useArticleLoader({ setIsCollapsed(true) try { - const article = await fetchArticleByNaddr(relayPool, naddr, false, settingsRef.current) - - if (!mountedRef.current) return - // Ignore if a newer request has started - if (currentRequestIdRef.current !== requestId) return - - setReaderContent({ - title: article.title, - markdown: article.markdown, - image: article.image, - summary: article.summary, - published: article.published, - url: `nostr:${naddr}` + // Decode naddr to filter + const decoded = nip19.decode(naddr) + if (decoded.type !== 'naddr') { + throw new Error('Invalid naddr format') + } + const pointer = decoded.data as AddressPointer + const filter = { + kinds: [pointer.kind], + authors: [pointer.pubkey], + '#d': [pointer.identifier] + } + + let firstEmitted = false + let latestEvent: NostrEvent | null = null + + // Stream local-first via queryEvents; rely on EOSE (no timeouts) + const events = await queryEvents(relayPool, filter, { + onEvent: (evt) => { + if (!mountedRef.current) return + if (currentRequestIdRef.current !== requestId) return + + // Store in event store for future local reads + try { eventStore?.add?.(evt as unknown as any) } catch {} + + // Keep latest by created_at + if (!latestEvent || evt.created_at > latestEvent.created_at) { + latestEvent = evt + } + + // Emit immediately on first event + if (!firstEmitted) { + firstEmitted = true + const title = Helpers.getArticleTitle(evt) || 'Untitled Article' + const image = Helpers.getArticleImage(evt) + const summary = Helpers.getArticleSummary(evt) + const published = Helpers.getArticlePublished(evt) + 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) + setReaderLoading(false) + } + } }) - - const dTag = article.event.tags.find(t => t[0] === 'd')?.[1] || '' - const articleCoordinate = `${article.event.kind}:${article.author}:${dTag}` - - setCurrentArticleCoordinate(articleCoordinate) - setCurrentArticleEventId(article.event.id) - setCurrentArticle?.(article.event) - setReaderLoading(false) - - // Fetch highlights asynchronously without blocking article display + + if (!mountedRef.current || currentRequestIdRef.current !== requestId) return + + // Finalize with newest version if it's newer than what we first rendered + const finalEvent = (events.sort((a, b) => b.created_at - a.created_at)[0]) || latestEvent + if (finalEvent) { + const title = Helpers.getArticleTitle(finalEvent) || 'Untitled Article' + const image = Helpers.getArticleImage(finalEvent) + const summary = Helpers.getArticleSummary(finalEvent) + const published = Helpers.getArticlePublished(finalEvent) + setReaderContent({ + title, + markdown: finalEvent.content, + image, + summary, + published, + url: `nostr:${naddr}` + }) + + const dTag = finalEvent.tags.find(t => t[0] === 'd')?.[1] || '' + const articleCoordinate = `${finalEvent.kind}:${finalEvent.pubkey}:${dTag}` + setCurrentArticleCoordinate(articleCoordinate) + setCurrentArticleEventId(finalEvent.id) + setCurrentArticle?.(finalEvent) + } else { + // As a last resort, fall back to the legacy helper (which includes cache) + const article = await fetchArticleByNaddr(relayPool, naddr, false, settingsRef.current) + if (!mountedRef.current || currentRequestIdRef.current !== requestId) return + setReaderContent({ + title: article.title, + markdown: article.markdown, + image: article.image, + summary: article.summary, + published: article.published, + url: `nostr:${naddr}` + }) + const dTag = article.event.tags.find(t => t[0] === 'd')?.[1] || '' + const articleCoordinate = `${article.event.kind}:${article.author}:${dTag}` + setCurrentArticleCoordinate(articleCoordinate) + setCurrentArticleEventId(article.event.id) + setCurrentArticle?.(article.event) + } + + // Fetch highlights after content is shown try { if (!mountedRef.current) return - setHighlightsLoading(true) setHighlights([]) - - await fetchHighlightsForArticle( - relayPool, - articleCoordinate, - article.event.id, - (highlight) => { - if (!mountedRef.current) return - // Ignore highlights from stale request - if (currentRequestIdRef.current !== requestId) 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 - ) + const le = latestEvent as NostrEvent | null + const dTag = le ? (le.tags.find((t: string[]) => t[0] === 'd')?.[1] || '') : '' + const coord = le && dTag ? `${le.kind}:${le.pubkey}:${dTag}` : undefined + const eventId = le ? le.id : undefined + if (coord && eventId) { + await fetchHighlightsForArticle( + relayPool, + coord, + eventId, + (highlight) => { + if (!mountedRef.current) return + if (currentRequestIdRef.current !== requestId) 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 + ) + } } catch (err) { console.error('Failed to fetch highlights:', err) } finally { @@ -134,6 +217,7 @@ export function useArticleLoader({ }, [ naddr, relayPool, + eventStore, setSelectedUrl, setReaderContent, setReaderLoading,