From 31f7d538291aec1cd33bdb0767046500a82cc0d7 Mon Sep 17 00:00:00 2001 From: Gigi Date: Sun, 12 Oct 2025 22:43:35 +0200 Subject: [PATCH] perf(explore): stream contacts + early posts from local; merge remote later --- src/components/Explore.tsx | 63 +++++++++++++++++++++------------ src/services/contactService.ts | 64 ++++++++++++++++++++-------------- 2 files changed, 78 insertions(+), 49 deletions(-) diff --git a/src/components/Explore.tsx b/src/components/Explore.tsx index e5087ac1..91ee344d 100644 --- a/src/components/Explore.tsx +++ b/src/components/Explore.tsx @@ -31,7 +31,44 @@ const Explore: React.FC = ({ relayPool }) => { setError(null) // Fetch the user's contacts (friends) - const contacts = await fetchContacts(relayPool, activeAccount.pubkey) + const contacts = await fetchContacts( + relayPool, + activeAccount.pubkey, + (partial) => { + // When local contacts are available, kick off early posts fetch + if (partial.size > 0) { + const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) + fetchBlogPostsFromAuthors( + relayPool, + Array.from(partial), + relayUrls, + (post) => { + setBlogPosts((prev) => { + const exists = prev.some(p => p.event.id === post.event.id) + if (exists) return prev + const next = [...prev, post] + return next.sort((a, b) => { + const timeA = a.published || a.event.created_at + const timeB = b.published || b.event.created_at + return timeB - timeA + }) + }) + } + ).then((all) => { + // Ensure union of streamed + final is displayed + setBlogPosts((prev) => { + const byId = new Map(prev.map(p => [p.event.id, p])) + for (const post of all) byId.set(post.event.id, post) + return Array.from(byId.values()).sort((a, b) => { + const timeA = a.published || a.event.created_at + const timeB = b.published || b.event.created_at + return timeB - timeA + }) + }) + }) + } + } + ) if (contacts.size === 0) { setError('You are not following anyone yet. Follow some people to see their blog posts!') @@ -39,29 +76,9 @@ const Explore: React.FC = ({ relayPool }) => { return } - // Get relay URLs from pool + // After full contacts, do a final pass for completeness const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) - - // Fetch blog posts from friends - const posts = await fetchBlogPostsFromAuthors( - relayPool, - Array.from(contacts), - relayUrls, - (post) => { - // Stream posts as we get them - setBlogPosts((prev) => { - const exists = prev.some(p => p.event.id === post.event.id) - if (exists) return prev - const next = [...prev, post] - // Keep sorted by published or created_at - return next.sort((a, b) => { - const timeA = a.published || a.event.created_at - const timeB = b.published || b.event.created_at - return timeB - timeA - }) - }) - } - ) + const posts = await fetchBlogPostsFromAuthors(relayPool, Array.from(contacts), relayUrls) if (posts.length === 0) { setError('No blog posts found from your friends yet') diff --git a/src/services/contactService.ts b/src/services/contactService.ts index dc69b3b2..0304c5e7 100644 --- a/src/services/contactService.ts +++ b/src/services/contactService.ts @@ -10,7 +10,8 @@ import { prioritizeLocalRelays } from '../utils/helpers' */ export const fetchContacts = async ( relayPool: RelayPool, - pubkey: string + pubkey: string, + onPartial?: (contacts: Set) => void ): Promise> => { try { const relayUrls = prioritizeLocalRelays(Array.from(relayPool.relays.values()).map(relay => relay.url)) @@ -31,35 +32,46 @@ export const fetchContacts = async ( events = [] } } - if (events.length === 0) { - events = await lastValueFrom( - relayPool - .req(relayUrls, { kinds: [3], authors: [pubkey] }) - .pipe(completeOnEose(), takeUntil(timer(6000)), toArray()) - ) + let followed = new Set() + if (events.length > 0) { + // Get the most recent contact list + const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) + const contactList = sortedEvents[0] + // Extract pubkeys from 'p' tags + for (const tag of contactList.tags) { + if (tag[0] === 'p' && tag[1]) { + followed.add(tag[1]) + } + } + if (onPartial) onPartial(new Set(followed)) + } + // Always fetch remote to merge more contacts + const remoteRelays = relayUrls.filter(url => !url.includes('localhost') && !url.includes('127.0.0.1')) + if (remoteRelays.length > 0) { + try { + const remoteEvents = await lastValueFrom( + relayPool + .req(remoteRelays, { kinds: [3], authors: [pubkey] }) + .pipe(completeOnEose(), takeUntil(timer(6000)), toArray()) + ) + if (remoteEvents.length > 0) { + const sortedEvents = remoteEvents.sort((a, b) => b.created_at - a.created_at) + const contactList = sortedEvents[0] + for (const tag of contactList.tags) { + if (tag[0] === 'p' && tag[1]) { + followed.add(tag[1]) + } + } + } + } catch { + // ignore + } } console.log('📊 Contact events fetched:', events.length) - if (events.length === 0) { - return new Set() - } - - // Get the most recent contact list - const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) - const contactList = sortedEvents[0] - - // Extract pubkeys from 'p' tags - const followedPubkeys = new Set() - for (const tag of contactList.tags) { - if (tag[0] === 'p' && tag[1]) { - followedPubkeys.add(tag[1]) - } - } - - console.log('👥 Followed contacts:', followedPubkeys.size) - - return followedPubkeys + console.log('👥 Followed contacts:', followed.size) + return followed } catch (error) { console.error('Failed to fetch contacts:', error) return new Set()