feat: replace user search service
This commit is contained in:
parent
400da44543
commit
b6cb701ff1
5 changed files with 86 additions and 84 deletions
|
|
@ -1,76 +1,48 @@
|
||||||
import { SEARCHABLE_RELAY_URLS } from '@/constants'
|
import { useInfiniteScroll } from '@/hooks'
|
||||||
import client from '@/services/client.service'
|
import fayan from '@/services/fayan.service'
|
||||||
import dayjs from 'dayjs'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import UserItem, { UserItemSkeleton } from '../UserItem'
|
import UserItem, { UserItemSkeleton } from '../UserItem'
|
||||||
|
|
||||||
const LIMIT = 50
|
const LIMIT = 50
|
||||||
|
const SHOW_COUNT = 20
|
||||||
|
|
||||||
export function ProfileListBySearch({ search }: { search: string }) {
|
export function ProfileListBySearch({ search }: { search: string }) {
|
||||||
const [until, setUntil] = useState<number>(() => dayjs().unix())
|
const [pubkeys, setPubkeys] = useState<string[]>([])
|
||||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
|
||||||
const [pubkeySet, setPubkeySet] = useState(new Set<string>())
|
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUntil(dayjs().unix())
|
setPubkeys([])
|
||||||
setHasMore(true)
|
|
||||||
setPubkeySet(new Set<string>())
|
|
||||||
loadMore()
|
|
||||||
}, [search])
|
}, [search])
|
||||||
|
|
||||||
useEffect(() => {
|
const handleLoadMore = useCallback(async () => {
|
||||||
if (!hasMore) return
|
const profiles = await fayan.searchUsers(search, LIMIT, pubkeys.length)
|
||||||
const options = {
|
if (profiles.length === 0) {
|
||||||
root: null,
|
return false
|
||||||
rootMargin: '10px',
|
|
||||||
threshold: 1
|
|
||||||
}
|
}
|
||||||
|
const pubkeySet = new Set(pubkeys)
|
||||||
const observerInstance = new IntersectionObserver((entries) => {
|
const newPubkeys = [...pubkeys]
|
||||||
if (entries[0].isIntersecting && hasMore) {
|
|
||||||
loadMore()
|
|
||||||
}
|
|
||||||
}, options)
|
|
||||||
|
|
||||||
const currentBottomRef = bottomRef.current
|
|
||||||
|
|
||||||
if (currentBottomRef) {
|
|
||||||
observerInstance.observe(currentBottomRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (observerInstance && currentBottomRef) {
|
|
||||||
observerInstance.unobserve(currentBottomRef)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [hasMore, search, until])
|
|
||||||
|
|
||||||
const loadMore = async () => {
|
|
||||||
const profiles = await client.searchProfiles(SEARCHABLE_RELAY_URLS, {
|
|
||||||
search,
|
|
||||||
until,
|
|
||||||
limit: LIMIT
|
|
||||||
})
|
|
||||||
const newPubkeySet = new Set<string>()
|
|
||||||
profiles.forEach((profile) => {
|
profiles.forEach((profile) => {
|
||||||
if (!pubkeySet.has(profile.pubkey)) {
|
if (!pubkeySet.has(profile.pubkey)) {
|
||||||
newPubkeySet.add(profile.pubkey)
|
pubkeySet.add(profile.pubkey)
|
||||||
|
newPubkeys.push(profile.pubkey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setPubkeySet((prev) => new Set([...prev, ...newPubkeySet]))
|
setPubkeys(newPubkeys)
|
||||||
setHasMore(profiles.length >= LIMIT)
|
return profiles.length >= LIMIT
|
||||||
const lastProfileCreatedAt = profiles[profiles.length - 1].created_at
|
}, [search, pubkeys])
|
||||||
setUntil(lastProfileCreatedAt ? lastProfileCreatedAt - 1 : 0)
|
|
||||||
}
|
const { visibleItems, shouldShowLoadingIndicator, bottomRef } = useInfiniteScroll({
|
||||||
|
items: pubkeys,
|
||||||
|
showCount: SHOW_COUNT,
|
||||||
|
onLoadMore: handleLoadMore
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
{Array.from(pubkeySet).map((pubkey, index) => (
|
{visibleItems.map((pubkey, index) => (
|
||||||
<UserItem key={`${index}-${pubkey}`} userId={pubkey} />
|
<UserItem key={`${index}-${pubkey}`} userId={pubkey} />
|
||||||
))}
|
))}
|
||||||
{hasMore && <UserItemSkeleton />}
|
<div ref={bottomRef} />
|
||||||
{hasMore && <div ref={bottomRef} />}
|
{shouldShowLoadingIndicator && <UserItemSkeleton />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ export const BIG_RELAY_URLS = [
|
||||||
export const SEARCHABLE_RELAY_URLS = [
|
export const SEARCHABLE_RELAY_URLS = [
|
||||||
'wss://search.nos.today/',
|
'wss://search.nos.today/',
|
||||||
'wss://relay.ditto.pub/',
|
'wss://relay.ditto.pub/',
|
||||||
'wss://relay.nostrcheck.me/',
|
|
||||||
'wss://relay.nostr.band/'
|
'wss://relay.nostr.band/'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { SEARCHABLE_RELAY_URLS } from '@/constants'
|
|
||||||
import { useFeed } from '@/providers/FeedProvider'
|
import { useFeed } from '@/providers/FeedProvider'
|
||||||
import client from '@/services/client.service'
|
import fayan from '@/services/fayan.service'
|
||||||
import { TProfile } from '@/types'
|
import { TProfile } from '@/types'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useFetchRelayInfos } from './useFetchRelayInfos'
|
import { useFetchRelayInfos } from './useFetchRelayInfos'
|
||||||
|
|
@ -22,19 +21,9 @@ export function useSearchProfiles(search: string, limit: number) {
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
setProfiles([])
|
setProfiles([])
|
||||||
try {
|
try {
|
||||||
const profiles = await client.searchProfilesFromLocal(search, limit)
|
const existingPubkeys = new Set<string>()
|
||||||
setProfiles(profiles)
|
const profiles: TProfile[] = []
|
||||||
if (profiles.length >= limit) {
|
const fetchedProfiles = await fayan.searchUsers(search, limit)
|
||||||
return
|
|
||||||
}
|
|
||||||
const existingPubkeys = new Set(profiles.map((profile) => profile.pubkey))
|
|
||||||
const fetchedProfiles = await client.searchProfiles(
|
|
||||||
searchableRelayUrls.concat(SEARCHABLE_RELAY_URLS).slice(0, 4),
|
|
||||||
{
|
|
||||||
search,
|
|
||||||
limit
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (fetchedProfiles.length) {
|
if (fetchedProfiles.length) {
|
||||||
fetchedProfiles.forEach((profile) => {
|
fetchedProfiles.forEach((profile) => {
|
||||||
if (existingPubkeys.has(profile.pubkey)) {
|
if (existingPubkeys.has(profile.pubkey)) {
|
||||||
|
|
|
||||||
|
|
@ -1031,18 +1031,6 @@ class ClientService extends EventTarget {
|
||||||
|
|
||||||
/** =========== Profile =========== */
|
/** =========== Profile =========== */
|
||||||
|
|
||||||
async searchProfiles(relayUrls: string[], filter: Filter): Promise<TProfile[]> {
|
|
||||||
const events = await this.query(relayUrls, {
|
|
||||||
...filter,
|
|
||||||
kinds: [kinds.Metadata]
|
|
||||||
})
|
|
||||||
|
|
||||||
const profileEvents = events.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
await Promise.allSettled(profileEvents.map((profile) => this.addUsernameToIndex(profile)))
|
|
||||||
profileEvents.forEach((profile) => this.updateProfileEventCache(profile))
|
|
||||||
return profileEvents.map((profileEvent) => getProfileFromEvent(profileEvent))
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchNpubsFromLocal(query: string, limit: number = 100) {
|
async searchNpubsFromLocal(query: string, limit: number = 100) {
|
||||||
const result = await this.userIndex.searchAsync(query, { limit })
|
const result = await this.userIndex.searchAsync(query, { limit })
|
||||||
return result.map((pubkey) => pubkeyToNpub(pubkey as string)).filter(Boolean) as string[]
|
return result.map((pubkey) => pubkeyToNpub(pubkey as string)).filter(Boolean) as string[]
|
||||||
|
|
@ -1170,7 +1158,10 @@ class ClientService extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProfileEventCache(event: NEvent) {
|
async updateProfileEventCache(event: NEvent) {
|
||||||
await this.updateReplaceableEventFromBigRelaysCache(event)
|
await Promise.allSettled([
|
||||||
|
this.updateReplaceableEventFromBigRelaysCache(event),
|
||||||
|
this.addUsernameToIndex(event)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** =========== Relay list =========== */
|
/** =========== Relay list =========== */
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
|
import { getProfileFromEvent } from '@/lib/event-metadata'
|
||||||
import { userIdToPubkey } from '@/lib/pubkey'
|
import { userIdToPubkey } from '@/lib/pubkey'
|
||||||
|
import { TProfile } from '@/types'
|
||||||
import DataLoader from 'dataloader'
|
import DataLoader from 'dataloader'
|
||||||
|
import { NostrEvent } from 'nostr-tools'
|
||||||
|
import client from './client.service'
|
||||||
|
|
||||||
|
const SERVICE_URL = 'https://fayan.jumble.social'
|
||||||
|
|
||||||
class FayanService {
|
class FayanService {
|
||||||
static instance: FayanService
|
static instance: FayanService
|
||||||
|
|
@ -7,7 +13,7 @@ class FayanService {
|
||||||
private userPercentileDataLoader = new DataLoader<string, number | null>(
|
private userPercentileDataLoader = new DataLoader<string, number | null>(
|
||||||
async (pubkeys) => {
|
async (pubkeys) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://fayan.jumble.social/users`, {
|
const res = await fetch(`${SERVICE_URL}/users`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|
@ -30,6 +36,7 @@ class FayanService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
private searchResultCache: Map<string, TProfile[]> = new Map()
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!FayanService.instance) {
|
if (!FayanService.instance) {
|
||||||
|
|
@ -42,6 +49,50 @@ class FayanService {
|
||||||
async fetchUserPercentile(userId: string): Promise<number | null> {
|
async fetchUserPercentile(userId: string): Promise<number | null> {
|
||||||
return await this.userPercentileDataLoader.load(userId)
|
return await this.userPercentileDataLoader.load(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async searchUsers(query: string, limit = 20, offset = 0) {
|
||||||
|
const cache = this.searchResultCache.get(query)
|
||||||
|
if (cache) {
|
||||||
|
if (offset + limit <= cache.length) {
|
||||||
|
console.log('FayanService searchUsers returning from cache')
|
||||||
|
return cache.slice(offset, offset + limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL('/search', SERVICE_URL)
|
||||||
|
url.searchParams.append('q', query)
|
||||||
|
url.searchParams.append('limit', limit.toString())
|
||||||
|
if (offset > 0) {
|
||||||
|
url.searchParams.append('offset', offset.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url.toString())
|
||||||
|
if (!res.ok) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as { event: NostrEvent; percentile: number }[]
|
||||||
|
const profiles: TProfile[] = []
|
||||||
|
data.forEach(({ event, percentile }) => {
|
||||||
|
const profile = getProfileFromEvent(event)
|
||||||
|
profiles.push(profile)
|
||||||
|
this.userPercentileDataLoader.prime(profile.pubkey, percentile)
|
||||||
|
client.updateProfileEventCache(event)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cache the results
|
||||||
|
const existingCache = this.searchResultCache.get(query) || []
|
||||||
|
if (offset === 0) {
|
||||||
|
this.searchResultCache.set(query, profiles)
|
||||||
|
} else if (offset <= existingCache.length) {
|
||||||
|
const newCache = existingCache.slice(0, offset).concat(profiles)
|
||||||
|
this.searchResultCache.set(query, newCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new FayanService()
|
const instance = new FayanService()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue