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.
 
 
 
 

210 lines
6.9 KiB

import {
fetchProfileAccordionBundle,
mergeProfileAccordionBundles,
profileAccordionBundleCacheKey,
type ProfileAccordionBundle
} from '@/lib/profile-accordion-fetch'
import {
profileAccordionGetCachedBadges,
profileAccordionGetCachedFollowPacks,
profileAccordionGetCachedInteractions,
profileAccordionGetCachedReports,
profileAccordionRelayUrlsKey,
profileAccordionSetBadges,
profileAccordionSetFollowPacks,
profileAccordionSetInteractions,
profileAccordionSetReports
} from '@/lib/profile-accordion-session-cache'
import { subtractNormalizedRelayUrls } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
const EMPTY: ProfileAccordionBundle = {
zaps: [],
reactions: [],
comments: [],
badges: [],
followPacks: [],
reports: []
}
function readFullCache(
pubkey: string,
relayKey: string,
viewerPubkey: string | null | undefined
): ProfileAccordionBundle | null {
const zi = profileAccordionGetCachedInteractions(pubkey, relayKey)
const zb = profileAccordionGetCachedBadges(pubkey, relayKey)
const zf = profileAccordionGetCachedFollowPacks(pubkey, relayKey)
const viewer = viewerPubkey?.trim()
const reportsReady = !viewer || profileAccordionGetCachedReports(pubkey, viewer) !== undefined
if (!zi || zb === undefined || zf === undefined || !reportsReady) return null
const reports =
viewer ? profileAccordionGetCachedReports(pubkey, viewer) ?? [] : []
return {
zaps: zi.zaps,
reactions: zi.reactions,
comments: zi.comments,
badges: zb,
followPacks: zf,
reports
}
}
/**
* Loads profile accordion data only when `enabled` (accordion open); hydrates from session cache first.
* Use {@link refresh} for manual network refresh.
*/
export function useProfileAccordionData(opts: {
pubkey: string | undefined
relayUrls: string[] | undefined
enabled: boolean
viewerPubkey: string | null | undefined
}) {
const { pubkey, relayUrls, enabled, viewerPubkey } = opts
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [data, setData] = useState<ProfileAccordionBundle>(EMPTY)
const [loading, setLoading] = useState(false)
const reqId = useRef(0)
const lastSuccessfulRelayUrlsRef = useRef<string[]>([])
// Keep refs so callbacks don't get recreated when these arrays change reference.
// Including live array references as useCallback deps causes the useLayoutEffect
// to re-fire and increment reqId, cancelling every in-flight fetch before it
// can commit its result — the accordion never shows data.
const relayUrlsRef = useRef(relayUrls)
relayUrlsRef.current = relayUrls
const favoriteRelaysRef = useRef(favoriteRelays)
favoriteRelaysRef.current = favoriteRelays
const blockedRelaysRef = useRef(blockedRelays)
blockedRelaysRef.current = blockedRelays
const relayKey = useMemo(
() => profileAccordionBundleCacheKey(relayUrls ?? []),
[relayUrls]
)
useEffect(() => {
lastSuccessfulRelayUrlsRef.current = []
}, [pubkey])
const runFetch = useCallback(
async (force: boolean, overrideUrls?: string[]) => {
const urls = (overrideUrls?.length ? overrideUrls : relayUrlsRef.current) ?? []
if (!pubkey?.trim() || !urls.length) return
const id = ++reqId.current
setLoading(true)
try {
const bundle = await fetchProfileAccordionBundle({
pubkey: pubkey.trim(),
urls,
viewerPubkey,
favoriteRelays: favoriteRelaysRef.current ?? [],
blockedRelays: blockedRelaysRef.current,
force,
onPartial: (partial) => {
if (id !== reqId.current) return
setData(partial)
}
})
if (id !== reqId.current) return
setData(bundle)
lastSuccessfulRelayUrlsRef.current = urls
} finally {
if (id === reqId.current) setLoading(false)
}
},
// relayUrls, favoriteRelays, and blockedRelays are read via refs — intentionally
// excluded from deps to prevent callback churn that cancels in-flight requests.
// eslint-disable-next-line react-hooks/exhaustive-deps
[pubkey, viewerPubkey]
)
const runMergeFetch = useCallback(
async (fullRelayUrls: string[], deltaUrls: string[], base: ProfileAccordionBundle) => {
const pk = pubkey?.trim()
if (!pk || !deltaUrls.length) return
const id = ++reqId.current
setLoading(true)
try {
const deltaB = await fetchProfileAccordionBundle({
pubkey: pk,
urls: deltaUrls,
viewerPubkey,
favoriteRelays: favoriteRelaysRef.current ?? [],
blockedRelays: blockedRelaysRef.current,
force: true,
onPartial: (partial) => {
if (id !== reqId.current) return
setData(mergeProfileAccordionBundles(base, partial))
}
})
if (id !== reqId.current) return
const merged = mergeProfileAccordionBundles(base, deltaB)
setData(merged)
const fullKey = profileAccordionBundleCacheKey(fullRelayUrls)
profileAccordionSetInteractions(pk, fullKey, {
zaps: merged.zaps,
reactions: merged.reactions,
comments: merged.comments
})
profileAccordionSetBadges(pk, fullKey, merged.badges)
profileAccordionSetFollowPacks(pk, fullKey, merged.followPacks)
const viewer = viewerPubkey?.trim()
if (viewer) profileAccordionSetReports(pk, viewer, merged.reports)
lastSuccessfulRelayUrlsRef.current = fullRelayUrls
} finally {
if (id === reqId.current) setLoading(false)
}
},
// favoriteRelays and blockedRelays are read via refs — see runFetch comment.
// eslint-disable-next-line react-hooks/exhaustive-deps
[pubkey, viewerPubkey]
)
const refresh = useCallback(
(overrideUrls?: string[]) => {
void runFetch(true, overrideUrls)
},
[runFetch]
)
useLayoutEffect(() => {
if (!enabled || !pubkey?.trim() || !relayUrls?.length) {
return
}
const pk = pubkey.trim()
const cached = readFullCache(pk, relayKey, viewerPubkey)
if (cached) {
setData(cached)
setLoading(false)
lastSuccessfulRelayUrlsRef.current = relayUrls
return
}
const prevSucc = lastSuccessfulRelayUrlsRef.current
if (
prevSucc.length > 0 &&
profileAccordionRelayUrlsKey(prevSucc) !== profileAccordionRelayUrlsKey(relayUrls)
) {
const delta = subtractNormalizedRelayUrls(relayUrls, prevSucc)
if (delta.length > 0) {
const prevKey = profileAccordionBundleCacheKey(prevSucc)
const base = readFullCache(pk, prevKey, viewerPubkey)
if (base) {
void runMergeFetch(relayUrls, delta, base)
return
}
}
}
setLoading(true)
void runFetch(false)
}, [enabled, pubkey, relayKey, relayUrls, viewerPubkey, runFetch, runMergeFetch])
return {
...data,
loading,
refresh
}
}