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.
127 lines
3.8 KiB
127 lines
3.8 KiB
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' |
|
import client from '@/services/client.service' |
|
import type { TProfile } from '@/types' |
|
import { useEffect, useMemo, useRef, useState } from 'react' |
|
import UserItem from '../UserItem' |
|
|
|
const PROFILE_CHUNK = 80 |
|
|
|
export default function ProfileList({ pubkeys }: { pubkeys: string[] }) { |
|
const [visiblePubkeys, setVisiblePubkeys] = useState<string[]>([]) |
|
const [profilesByPubkey, setProfilesByPubkey] = useState<Map<string, TProfile>>(() => new Map()) |
|
const bottomRef = useRef<HTMLDivElement>(null) |
|
const loadedRef = useRef<Set<string>>(new Set()) |
|
const batchGenRef = useRef(0) |
|
const pubkeysKey = useMemo(() => pubkeys.join('\u0001'), [pubkeys]) |
|
|
|
useEffect(() => { |
|
setVisiblePubkeys(pubkeys.slice(0, 10)) |
|
}, [pubkeysKey, pubkeys]) |
|
|
|
useEffect(() => { |
|
const options = { |
|
root: null, |
|
rootMargin: '10px', |
|
threshold: 1 |
|
} |
|
|
|
const observerInstance = new IntersectionObserver((entries) => { |
|
if (entries[0].isIntersecting && pubkeys.length > visiblePubkeys.length) { |
|
setVisiblePubkeys((prev) => [...prev, ...pubkeys.slice(prev.length, prev.length + 10)]) |
|
} |
|
}, options) |
|
|
|
const currentBottomRef = bottomRef.current |
|
if (currentBottomRef) { |
|
observerInstance.observe(currentBottomRef) |
|
} |
|
|
|
return () => { |
|
if (observerInstance && currentBottomRef) { |
|
observerInstance.unobserve(currentBottomRef) |
|
} |
|
} |
|
}, [visiblePubkeys, pubkeysKey, pubkeys]) |
|
|
|
const visibleHexPubkeysKey = useMemo( |
|
() => |
|
visiblePubkeys |
|
.filter((pk) => pk.length === 64 && /^[0-9a-f]{64}$/i.test(pk)) |
|
.map((pk) => pk.toLowerCase()) |
|
.join('\u0001'), |
|
[visiblePubkeys] |
|
) |
|
|
|
useEffect(() => { |
|
const need = visibleHexPubkeysKey |
|
.split('\u0001') |
|
.filter(Boolean) |
|
.filter((pk) => !loadedRef.current.has(pk)) |
|
if (need.length === 0) return |
|
|
|
const gen = ++batchGenRef.current |
|
need.forEach((pk) => loadedRef.current.add(pk)) |
|
|
|
void (async () => { |
|
const chunks: string[][] = [] |
|
for (let i = 0; i < need.length; i += PROFILE_CHUNK) { |
|
chunks.push(need.slice(i, i + PROFILE_CHUNK)) |
|
} |
|
const settled = await Promise.allSettled( |
|
chunks.map((chunk) => client.fetchProfilesForPubkeys(chunk)) |
|
) |
|
if (gen !== batchGenRef.current) return |
|
|
|
setProfilesByPubkey((prev) => { |
|
const next = new Map(prev) |
|
settled.forEach((res, idx) => { |
|
const chunk = chunks[idx]! |
|
if (res.status === 'rejected') { |
|
chunk.forEach((pk) => loadedRef.current.delete(pk)) |
|
return |
|
} |
|
for (const p of res.value) { |
|
const pkNorm = p.pubkey.toLowerCase() |
|
next.set(pkNorm, { ...p, pubkey: pkNorm }) |
|
} |
|
for (const pk of chunk) { |
|
const pkNorm = pk.toLowerCase() |
|
if (!next.has(pkNorm)) { |
|
next.set(pkNorm, { |
|
pubkey: pkNorm, |
|
npub: pubkeyToNpub(pkNorm) ?? '', |
|
username: formatPubkey(pkNorm), |
|
batchPlaceholder: true |
|
}) |
|
} |
|
} |
|
}) |
|
return next |
|
}) |
|
})() |
|
}, [visibleHexPubkeysKey]) |
|
|
|
useEffect(() => { |
|
batchGenRef.current += 1 |
|
loadedRef.current.clear() |
|
setProfilesByPubkey(new Map()) |
|
}, [pubkeysKey]) |
|
|
|
return ( |
|
<div className="px-4 pt-2"> |
|
{visiblePubkeys.map((pubkey, index) => { |
|
const pkNorm = pubkey.length === 64 ? pubkey.toLowerCase() : pubkey |
|
const prefetchedProfile = profilesByPubkey.get(pkNorm) |
|
return ( |
|
<UserItem |
|
key={`${index}-${pubkey}`} |
|
pubkey={pubkey} |
|
prefetchedProfile={prefetchedProfile} |
|
deferRemoteAvatar={false} |
|
/> |
|
) |
|
})} |
|
{pubkeys.length > visiblePubkeys.length && <div ref={bottomRef} />} |
|
</div> |
|
) |
|
}
|
|
|