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.
195 lines
5.7 KiB
195 lines
5.7 KiB
import { SEARCHABLE_RELAY_URLS, THREAD_CONTEXT_EVENT_FETCH_GLOBAL_TIMEOUT_MS } from '@/constants' |
|
import { sanitizeRelayUrlsForFetch } from '@/lib/read-only-relay-personal' |
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' |
|
import { useDeletedEvent } from '@/providers/DeletedEventProvider' |
|
import { useNostr } from '@/providers/NostrProvider' |
|
import { useReply } from '@/providers/ReplyProvider' |
|
import { getNoteBech32Id, getParentETag, getRootETag } from '@/lib/event' |
|
import { buildThreadContextFetchRelayUrls } from '@/lib/thread-context-relays' |
|
import client, { eventService } from '@/services/client.service' |
|
import { navigationEventStore } from '@/services/navigation-event-store' |
|
import { Event } from 'nostr-tools' |
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' |
|
|
|
export type ThreadContextRole = 'parent' | 'root' |
|
|
|
export function useFetchThreadContextEvent( |
|
eventId: string | undefined, |
|
contextEvent: Event | undefined, |
|
role: ThreadContextRole, |
|
initialEvent?: Event |
|
) { |
|
const { pubkey: viewerPubkey } = useNostr() |
|
const { blockedRelays } = useFavoriteRelays() |
|
const { isEventDeleted } = useDeletedEvent() |
|
const { addReplies } = useReply() |
|
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 searchableAttemptedRef = useRef(false) |
|
|
|
const refetch = useCallback(() => { |
|
searchableAttemptedRef.current = false |
|
setRefetchToken((n) => n + 1) |
|
}, []) |
|
|
|
const targetTag = useMemo(() => { |
|
if (!contextEvent) return undefined |
|
return role === 'parent' ? getParentETag(contextEvent) : getRootETag(contextEvent) |
|
}, [contextEvent, role]) |
|
|
|
const blockedKey = useMemo( |
|
() => [...blockedRelays].map((u) => u).sort().join('\0'), |
|
[blockedRelays] |
|
) |
|
|
|
useEffect(() => { |
|
searchableAttemptedRef.current = false |
|
}, [eventId]) |
|
|
|
useEffect(() => { |
|
let cancelled = false |
|
|
|
if (!eventId || !contextEvent) { |
|
setIsFetching(false) |
|
setEvent(initialEvent) |
|
return () => { |
|
cancelled = true |
|
} |
|
} |
|
|
|
const skipShortcuts = refetchToken > 0 |
|
|
|
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 |
|
} |
|
} |
|
|
|
if (!skipShortcuts) { |
|
const navigationEvent = navigationEventStore.peekEvent(eventId) |
|
if (navigationEvent && !isEventDeleted(navigationEvent)) { |
|
setEvent(navigationEvent) |
|
addReplies([navigationEvent]) |
|
setIsFetching(false) |
|
return () => { |
|
cancelled = true |
|
} |
|
} |
|
} |
|
|
|
setEvent(undefined) |
|
setError(null) |
|
setIsFetching(true) |
|
|
|
const fetchWithFallback = async () => { |
|
try { |
|
const relayUrls = await buildThreadContextFetchRelayUrls( |
|
contextEvent, |
|
targetTag, |
|
viewerPubkey ?? undefined, |
|
blockedRelays |
|
) |
|
const threadOpts = relayUrls.length |
|
? { relayHints: relayUrls, threadContext: true as const } |
|
: { threadContext: true as const } |
|
|
|
const fetchParentOrRoot = async () => { |
|
if (skipShortcuts) { |
|
return eventService.fetchEventForceRetry(eventId, threadOpts) |
|
} |
|
return eventService.fetchEvent(eventId, threadOpts) |
|
} |
|
|
|
let fetchedEvent = await Promise.race([ |
|
fetchParentOrRoot(), |
|
new Promise<undefined>((resolve) => { |
|
window.setTimeout(() => resolve(undefined), THREAD_CONTEXT_EVENT_FETCH_GLOBAL_TIMEOUT_MS) |
|
}) |
|
]) |
|
|
|
if ( |
|
!fetchedEvent && |
|
!searchableAttemptedRef.current && |
|
SEARCHABLE_RELAY_URLS.length > 0 |
|
) { |
|
searchableAttemptedRef.current = true |
|
const searchable = sanitizeRelayUrlsForFetch([...SEARCHABLE_RELAY_URLS]) |
|
fetchedEvent = await Promise.race([ |
|
client.fetchEventWithExternalRelays(eventId, searchable), |
|
new Promise<undefined>((resolve) => { |
|
window.setTimeout(() => resolve(undefined), THREAD_CONTEXT_EVENT_FETCH_GLOBAL_TIMEOUT_MS) |
|
}) |
|
]) |
|
if (fetchedEvent) { |
|
client.addEventToCache(fetchedEvent) |
|
} |
|
} |
|
|
|
if (cancelled) return |
|
if (fetchedEvent && !isEventDeleted(fetchedEvent)) { |
|
setEvent(fetchedEvent) |
|
addReplies([fetchedEvent]) |
|
} else { |
|
setEvent(undefined) |
|
} |
|
} catch (err) { |
|
if (!cancelled) { |
|
setError(err as Error) |
|
setEvent(undefined) |
|
} |
|
} finally { |
|
if (!cancelled) { |
|
setIsFetching(false) |
|
} |
|
} |
|
} |
|
|
|
void fetchWithFallback() |
|
|
|
return () => { |
|
cancelled = true |
|
setIsFetching(false) |
|
} |
|
}, [ |
|
eventId, |
|
contextEvent, |
|
targetTag, |
|
initialEvent, |
|
isEventDeleted, |
|
addReplies, |
|
refetchToken, |
|
viewerPubkey, |
|
blockedKey, |
|
role |
|
]) |
|
|
|
useEffect(() => { |
|
if (event && isEventDeleted(event)) { |
|
setEvent(undefined) |
|
} |
|
}, [isEventDeleted, event]) |
|
|
|
useEffect(() => { |
|
if (!eventId || event !== undefined) return undefined |
|
return eventService.subscribeWhenSessionHasEvent(eventId, refetch) |
|
}, [eventId, event, refetch]) |
|
|
|
return { isFetching, error, event, refetch } |
|
}
|
|
|