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

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>
)
}