diff --git a/src/components/ProfileListBySearch/index.tsx b/src/components/ProfileListBySearch/index.tsx index 9f84a02..7b0e852 100644 --- a/src/components/ProfileListBySearch/index.tsx +++ b/src/components/ProfileListBySearch/index.tsx @@ -1,76 +1,48 @@ -import { SEARCHABLE_RELAY_URLS } from '@/constants' -import client from '@/services/client.service' -import dayjs from 'dayjs' -import { useEffect, useRef, useState } from 'react' +import { useInfiniteScroll } from '@/hooks' +import fayan from '@/services/fayan.service' +import { useCallback, useEffect, useState } from 'react' import UserItem, { UserItemSkeleton } from '../UserItem' const LIMIT = 50 +const SHOW_COUNT = 20 export function ProfileListBySearch({ search }: { search: string }) { - const [until, setUntil] = useState(() => dayjs().unix()) - const [hasMore, setHasMore] = useState(true) - const [pubkeySet, setPubkeySet] = useState(new Set()) - const bottomRef = useRef(null) + const [pubkeys, setPubkeys] = useState([]) useEffect(() => { - setUntil(dayjs().unix()) - setHasMore(true) - setPubkeySet(new Set()) - loadMore() + setPubkeys([]) }, [search]) - useEffect(() => { - if (!hasMore) return - const options = { - root: null, - rootMargin: '10px', - threshold: 1 + const handleLoadMore = useCallback(async () => { + const profiles = await fayan.searchUsers(search, LIMIT, pubkeys.length) + if (profiles.length === 0) { + return false } - - const observerInstance = new IntersectionObserver((entries) => { - 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() + const pubkeySet = new Set(pubkeys) + const newPubkeys = [...pubkeys] profiles.forEach((profile) => { if (!pubkeySet.has(profile.pubkey)) { - newPubkeySet.add(profile.pubkey) + pubkeySet.add(profile.pubkey) + newPubkeys.push(profile.pubkey) } }) - setPubkeySet((prev) => new Set([...prev, ...newPubkeySet])) - setHasMore(profiles.length >= LIMIT) - const lastProfileCreatedAt = profiles[profiles.length - 1].created_at - setUntil(lastProfileCreatedAt ? lastProfileCreatedAt - 1 : 0) - } + setPubkeys(newPubkeys) + return profiles.length >= LIMIT + }, [search, pubkeys]) + + const { visibleItems, shouldShowLoadingIndicator, bottomRef } = useInfiniteScroll({ + items: pubkeys, + showCount: SHOW_COUNT, + onLoadMore: handleLoadMore + }) return (
- {Array.from(pubkeySet).map((pubkey, index) => ( + {visibleItems.map((pubkey, index) => ( ))} - {hasMore && } - {hasMore &&
} +
+ {shouldShowLoadingIndicator && }
) } diff --git a/src/constants.ts b/src/constants.ts index d05a1b9..321d10e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -76,7 +76,6 @@ export const BIG_RELAY_URLS = [ export const SEARCHABLE_RELAY_URLS = [ 'wss://search.nos.today/', 'wss://relay.ditto.pub/', - 'wss://relay.nostrcheck.me/', 'wss://relay.nostr.band/' ] diff --git a/src/hooks/useSearchProfiles.tsx b/src/hooks/useSearchProfiles.tsx index 512e4c6..45d9d93 100644 --- a/src/hooks/useSearchProfiles.tsx +++ b/src/hooks/useSearchProfiles.tsx @@ -1,6 +1,5 @@ -import { SEARCHABLE_RELAY_URLS } from '@/constants' import { useFeed } from '@/providers/FeedProvider' -import client from '@/services/client.service' +import fayan from '@/services/fayan.service' import { TProfile } from '@/types' import { useEffect, useState } from 'react' import { useFetchRelayInfos } from './useFetchRelayInfos' @@ -22,19 +21,9 @@ export function useSearchProfiles(search: string, limit: number) { setIsFetching(true) setProfiles([]) try { - const profiles = await client.searchProfilesFromLocal(search, limit) - setProfiles(profiles) - if (profiles.length >= 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 - } - ) + const existingPubkeys = new Set() + const profiles: TProfile[] = [] + const fetchedProfiles = await fayan.searchUsers(search, limit) if (fetchedProfiles.length) { fetchedProfiles.forEach((profile) => { if (existingPubkeys.has(profile.pubkey)) { diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 19b43ec..08e205a 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -1031,18 +1031,6 @@ class ClientService extends EventTarget { /** =========== Profile =========== */ - async searchProfiles(relayUrls: string[], filter: Filter): Promise { - 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) { const result = await this.userIndex.searchAsync(query, { limit }) return result.map((pubkey) => pubkeyToNpub(pubkey as string)).filter(Boolean) as string[] @@ -1170,7 +1158,10 @@ class ClientService extends EventTarget { } async updateProfileEventCache(event: NEvent) { - await this.updateReplaceableEventFromBigRelaysCache(event) + await Promise.allSettled([ + this.updateReplaceableEventFromBigRelaysCache(event), + this.addUsernameToIndex(event) + ]) } /** =========== Relay list =========== */ diff --git a/src/services/fayan.service.ts b/src/services/fayan.service.ts index 85ccb84..b2b5928 100644 --- a/src/services/fayan.service.ts +++ b/src/services/fayan.service.ts @@ -1,5 +1,11 @@ +import { getProfileFromEvent } from '@/lib/event-metadata' import { userIdToPubkey } from '@/lib/pubkey' +import { TProfile } from '@/types' import DataLoader from 'dataloader' +import { NostrEvent } from 'nostr-tools' +import client from './client.service' + +const SERVICE_URL = 'https://fayan.jumble.social' class FayanService { static instance: FayanService @@ -7,7 +13,7 @@ class FayanService { private userPercentileDataLoader = new DataLoader( async (pubkeys) => { try { - const res = await fetch(`https://fayan.jumble.social/users`, { + const res = await fetch(`${SERVICE_URL}/users`, { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -30,6 +36,7 @@ class FayanService { } } ) + private searchResultCache: Map = new Map() constructor() { if (!FayanService.instance) { @@ -42,6 +49,50 @@ class FayanService { async fetchUserPercentile(userId: string): Promise { 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()