You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

136 lines
4.6 KiB

import { getNoteBech32Id } from '@/lib/event'
import { useIsEventDeleted } from '@/providers/DeletedEventProvider'
import { useReplyIngress } from '@/hooks/useReplyIngress'
import { eventService } from '@/services/client.service'
import { navigationEventStore } from '@/services/navigation-event-store'
import { Event } from 'nostr-tools'
import { useCallback, useEffect, useState } from 'react'
export function useFetchEvent(
eventId?: string,
initialEvent?: Event,
fetchOpts?: { relayHints?: string[] }
) {
const isEventDeleted = useIsEventDeleted()
const { addReplies } = useReplyIngress()
const [error, setError] = useState<Error | null>(null)
const [event, setEvent] = useState<Event | undefined>(initialEvent)
const [isFetching, setIsFetching] = useState(!initialEvent)
const [refetchToken, setRefetchToken] = useState(0)
const refetch = useCallback(() => {
setRefetchToken((n) => n + 1)
}, [])
/** Content-based key so a new `relayHints` array with the same URLs does not restart the fetch. */
const relayHintsSerialized = fetchOpts?.relayHints?.join('\0') ?? ''
useEffect(() => {
let cancelled = false
if (!eventId) {
setIsFetching(false)
setEvent(undefined)
// Do not setError here: this effect re-runs when callback deps (e.g. addReplies) change identity;
// allocating a new Error each time would force updates and can exceed React's max update depth.
return () => {
cancelled = true
}
}
const skipShortcuts = refetchToken > 0
// If we have an initial event that matches the eventId, use it and skip fetching
const initialMatches =
initialEvent &&
(initialEvent.id === eventId ||
(() => {
try {
return getNoteBech32Id(initialEvent) === eventId
} catch {
return false
}
})())
if (!skipShortcuts && initialMatches && initialEvent) {
if (!isEventDeleted(initialEvent)) {
setEvent(initialEvent)
addReplies([initialEvent])
setIsFetching(false)
}
return () => {
cancelled = true
}
}
// Check navigation event store first (events passed through navigation) — peek so remounts still see it.
if (!skipShortcuts) {
const navigationEvent = navigationEventStore.peekEvent(eventId)
if (navigationEvent && !isEventDeleted(navigationEvent)) {
setEvent(navigationEvent)
addReplies([navigationEvent])
setIsFetching(false)
return () => {
cancelled = true
}
}
}
// New target without a synchronous hit: drop the previous note immediately so the panel does not
// keep showing the last-opened article (or fail to show a skeleton) while the new fetch runs or
// after it returns empty.
setEvent(undefined)
setError(null)
setIsFetching(true)
const fetchEvent = async () => {
try {
// First load: DataLoader dedupes. Refetches (incl. session-waiter) clear a prior undefined so
// timeline-cached events resolve after the embed mounted first.
const opts = fetchOpts?.relayHints?.length ? fetchOpts : undefined
const fetchedEvent = skipShortcuts
? await eventService.fetchEventForceRetry(eventId, opts)
: await eventService.fetchEvent(eventId, opts)
if (cancelled) return
if (fetchedEvent && !isEventDeleted(fetchedEvent)) {
setEvent(fetchedEvent)
addReplies([fetchedEvent])
} else {
setEvent(undefined)
}
} catch (error) {
if (!cancelled) {
setError(error as Error)
setEvent(undefined)
}
} finally {
if (!cancelled) {
setIsFetching(false)
}
}
}
void fetchEvent()
return () => {
cancelled = true
// If deps change (e.g. embed relay hints) or Strict Mode re-runs the effect while a fetch is
// still in flight, `finally` skips `setIsFetching(false)` when `cancelled` — without this,
// loading can stay true forever and embeds show an endless skeleton.
setIsFetching(false)
}
}, [eventId, initialEvent, isEventDeleted, addReplies, refetchToken, relayHintsSerialized])
useEffect(() => {
if (event && isEventDeleted(event)) {
setEvent(undefined)
}
}, [isEventDeleted, event])
// Parent notes often render before the embedded event arrives from the same timeline; refetch when it hits session cache.
useEffect(() => {
if (!eventId || event !== undefined) return undefined
return eventService.subscribeWhenSessionHasEvent(eventId, refetch)
}, [eventId, event, refetch])
return { isFetching, error, event, refetch }
}