From 8824e8f8435ca4e579af6aee55b8d6f4d9f7b317 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 23 Mar 2026 06:38:28 +0100 Subject: [PATCH] speed up profile feed --- src/hooks/useProfilePins.tsx | 76 ++++++++++++++++---------------- src/hooks/useProfileTimeline.tsx | 35 ++++----------- src/lib/favorites-feed-relays.ts | 24 ++++++++++ 3 files changed, 72 insertions(+), 63 deletions(-) diff --git a/src/hooks/useProfilePins.tsx b/src/hooks/useProfilePins.tsx index 5702a1bd..63e8a204 100644 --- a/src/hooks/useProfilePins.tsx +++ b/src/hooks/useProfilePins.tsx @@ -1,12 +1,13 @@ import { useCallback, useEffect, useState } from 'react' import { Event } from 'nostr-tools' -import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' +import { + buildProfilePageReadRelayUrls, + PROFILE_PAGE_PINS_RESOLVE_LIMIT +} from '@/lib/favorites-feed-relays' import logger from '@/lib/logger' -import { normalizeUrl } from '@/lib/url' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' -import { queryService, replaceableEventService } from '@/services/client.service' +import { queryService } from '@/services/client.service' const CACHE_DURATION = 5 * 60 * 1000 @@ -57,24 +58,10 @@ function orderPinEvents(pinList: Event, eventsById: Map): Event[] } export function useProfilePins(pubkey: string | undefined) { - const { pubkey: myPubkey } = useNostr() - const { favoriteRelays } = useFavoriteRelays() + const { favoriteRelays, blockedRelays } = useFavoriteRelays() const [pinEvents, setPinEvents] = useState([]) const [loadingPins, setLoadingPins] = useState(false) - const buildComprehensiveRelayList = useCallback(async () => { - const myRelayList = myPubkey ? await client.fetchRelayList(myPubkey) : { write: [], read: [] } - const allRelays = [ - ...(myRelayList.read || []), - ...(myRelayList.write || []), - ...(favoriteRelays || []), - ...FAST_READ_RELAY_URLS, - ...FAST_WRITE_RELAY_URLS - ] - const normalized = allRelays.map((url) => normalizeUrl(url)).filter((url): url is string => !!url) - return Array.from(new Set(normalized)) - }, [myPubkey, favoriteRelays]) - const loadPins = useCallback( async (forceRefresh = false) => { if (!pubkey) { @@ -93,32 +80,48 @@ export function useProfilePins(pubkey: string | undefined) { setLoadingPins(true) try { - const comprehensiveRelays = await buildComprehensiveRelayList() - let pinList: Event | null = null - try { - const pinListEvents = await queryService.fetchEvents(comprehensiveRelays, { - authors: [pubkey], - kinds: [10001], - limit: 1 - }) - pinList = pinListEvents[0] || null - } catch { - pinList = (await replaceableEventService.fetchReplaceableEvent(pubkey, 10001)) ?? null + const authorRl = await client.fetchRelayList(pubkey).catch(() => ({ + read: [] as string[], + write: [] as string[] + })) + const profileRelays = buildProfilePageReadRelayUrls( + favoriteRelays, + blockedRelays, + authorRl, + false + ) + if (!profileRelays.length) { + setPinEvents([]) + pinsCache.set(cacheKey, { events: [], lastUpdated: Date.now() }) + return } + const pinListEvents = await queryService.fetchEvents(profileRelays, { + authors: [pubkey], + kinds: [10001], + limit: 1 + }) + const pinList: Event | null = pinListEvents[0] || null + if (!pinList?.tags?.length) { setPinEvents([]) pinsCache.set(cacheKey, { events: [], lastUpdated: Date.now() }) return } - const eventIds = pinList.tags.filter((tag) => tag[0] === 'e' && tag[1]).map((tag) => tag[1]) - const aTags = pinList.tags.filter((tag) => tag[0] === 'a' && tag[1]).map((tag) => tag[1]) + const max = PROFILE_PAGE_PINS_RESOLVE_LIMIT + const eventIds: string[] = [] + const aTags: string[] = [] + for (const tag of pinList.tags) { + if (eventIds.length + aTags.length >= max) break + if (tag[0] === 'e' && tag[1]) eventIds.push(tag[1]) + else if (tag[0] === 'a' && tag[1]) aTags.push(tag[1]) + } const eventPromises: Promise[] = [] if (eventIds.length > 0) { eventPromises.push( - queryService.fetchEvents(comprehensiveRelays, { ids: eventIds, limit: 100 }) + queryService.fetchEvents(profileRelays, { ids: eventIds, limit: max }) ) } if (aTags.length > 0) { @@ -131,7 +134,7 @@ export function useProfilePins(pubkey: string | undefined) { const filter = d ? { authors: [author], kinds: [kind], limit: 1, '#d': [d] as [string] } : { authors: [author], kinds: [kind], limit: 1 } - const events = await queryService.fetchEvents(comprehensiveRelays, [filter]) + const events = await queryService.fetchEvents(profileRelays, [filter]) return events[0] || null }) eventPromises.push( @@ -158,7 +161,7 @@ export function useProfilePins(pubkey: string | undefined) { setLoadingPins(false) } }, - [pubkey, buildComprehensiveRelayList] + [pubkey, favoriteRelays, blockedRelays] ) useEffect(() => { @@ -166,8 +169,7 @@ export function useProfilePins(pubkey: string | undefined) { setPinEvents([]) return } - const t = setTimeout(() => void loadPins(false), 200) - return () => clearTimeout(t) + void loadPins(false) }, [pubkey, loadPins]) const refreshPins = useCallback(() => { diff --git a/src/hooks/useProfileTimeline.tsx b/src/hooks/useProfileTimeline.tsx index c449ecc5..907e6bda 100644 --- a/src/hooks/useProfileTimeline.tsx +++ b/src/hooks/useProfileTimeline.tsx @@ -3,9 +3,8 @@ import client from '@/services/client.service' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Event } from 'nostr-tools' import { CALENDAR_EVENT_KINDS, ExtendedKind } from '@/constants' -import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays' +import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import { useNostr } from '@/providers/NostrProvider' type ProfileTimelineMemoryEntry = { events: Event[] @@ -91,7 +90,6 @@ export function useProfileTimeline({ filterPredicate }: UseProfileTimelineOptions): UseProfileTimelineResult { const { favoriteRelays, blockedRelays } = useFavoriteRelays() - const { relayList } = useNostr() const { isEventDeleted, tombstoneEpoch } = useDeletedEvent() const isEventDeletedRef = useRef(isEventDeleted) isEventDeletedRef.current = isEventDeleted @@ -105,21 +103,8 @@ export function useProfileTimeline({ const [events, setEvents] = useState(cachedEntry?.events ?? []) const [isLoading, setIsLoading] = useState(!cachedEntry) const [refreshToken, setRefreshToken] = useState(0) - const [authorOutboxWrite, setAuthorOutboxWrite] = useState([]) const subscriptionRef = useRef<() => void>(() => {}) - useEffect(() => { - let cancelled = false - setAuthorOutboxWrite([]) - void client.fetchRelayList(pubkey).then((rl) => { - if (cancelled || !rl?.write?.length) return - setAuthorOutboxWrite(rl.write) - }) - return () => { - cancelled = true - } - }, [pubkey]) - useEffect(() => { setEvents((prev) => { const next = prev.filter((e) => !isEventDeletedRef.current(e)) @@ -178,15 +163,15 @@ export function useProfileTimeline({ } const hasCalendarKinds = kinds.some((k) => CALENDAR_EVENT_KINDS.includes(k)) - const feedRelayUrls = getRelayUrlsWithFavoritesFastReadAndInbox( + const authorRl = await client.fetchRelayList(pubkey).catch(() => ({ + read: [] as string[], + write: [] as string[] + })) + const feedRelayUrls = buildProfilePageReadRelayUrls( favoriteRelays, blockedRelays, - relayList?.read ?? [], - { - userWriteRelays: relayList?.write ?? [], - authorWriteRelays: authorOutboxWrite, - applyKind1BlockedFilter: kinds.includes(1) - } + authorRl, + kinds.includes(1) ) const startWave = async (subRequests: ReturnType) => { @@ -239,9 +224,7 @@ export function useProfileTimeline({ filterPredicate, refreshToken, favoriteRelays, - blockedRelays, - relayList, - authorOutboxWrite + blockedRelays ]) const refresh = useCallback(() => { diff --git a/src/lib/favorites-feed-relays.ts b/src/lib/favorites-feed-relays.ts index c84b63a9..e378deda 100644 --- a/src/lib/favorites-feed-relays.ts +++ b/src/lib/favorites-feed-relays.ts @@ -111,6 +111,30 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox( }) } +/** Profile page pins + feed: author's NIP-65 read/write, then favorites, then fast-read defaults, capped. */ +export const PROFILE_PAGE_FEED_MAX_RELAYS = 6 + +export const PROFILE_PAGE_PINS_RESOLVE_LIMIT = 10 + +export function buildProfilePageReadRelayUrls( + favoriteRelays: string[], + blockedRelays: string[], + authorRelayList: { read: string[]; write: string[] }, + kindsIncludeKind1: boolean +): string[] { + return getRelayUrlsWithFavoritesFastReadAndInbox( + favoriteRelays, + blockedRelays, + authorRelayList.read ?? [], + { + userWriteRelays: authorRelayList.write ?? [], + authorWriteRelays: [], + maxRelays: PROFILE_PAGE_FEED_MAX_RELAYS, + applyKind1BlockedFilter: kindsIncludeKind1 + } + ) +} + /** * Per subrequest: shared inbox → author/favorites → fast read stack, normalized, user-blocked and (when applicable) * kind-1-blocked stripped, deduped, capped. Subrequest `urls` are prepended first by default (following shards);