Browse Source

speed up profile feed

imwald
Silberengel 1 month ago
parent
commit
8824e8f843
  1. 70
      src/hooks/useProfilePins.tsx
  2. 35
      src/hooks/useProfileTimeline.tsx
  3. 24
      src/lib/favorites-feed-relays.ts

70
src/hooks/useProfilePins.tsx

@ -1,12 +1,13 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Event } from 'nostr-tools' 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 logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service' 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 const CACHE_DURATION = 5 * 60 * 1000
@ -57,24 +58,10 @@ function orderPinEvents(pinList: Event, eventsById: Map<string, Event>): Event[]
} }
export function useProfilePins(pubkey: string | undefined) { export function useProfilePins(pubkey: string | undefined) {
const { pubkey: myPubkey } = useNostr() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { favoriteRelays } = useFavoriteRelays()
const [pinEvents, setPinEvents] = useState<Event[]>([]) const [pinEvents, setPinEvents] = useState<Event[]>([])
const [loadingPins, setLoadingPins] = useState(false) 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( const loadPins = useCallback(
async (forceRefresh = false) => { async (forceRefresh = false) => {
if (!pubkey) { if (!pubkey) {
@ -93,18 +80,28 @@ export function useProfilePins(pubkey: string | undefined) {
setLoadingPins(true) setLoadingPins(true)
try { try {
const comprehensiveRelays = await buildComprehensiveRelayList() const authorRl = await client.fetchRelayList(pubkey).catch(() => ({
let pinList: Event | null = null read: [] as string[],
try { write: [] as string[]
const pinListEvents = await queryService.fetchEvents(comprehensiveRelays, { }))
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], authors: [pubkey],
kinds: [10001], kinds: [10001],
limit: 1 limit: 1
}) })
pinList = pinListEvents[0] || null const pinList: Event | null = pinListEvents[0] || null
} catch {
pinList = (await replaceableEventService.fetchReplaceableEvent(pubkey, 10001)) ?? null
}
if (!pinList?.tags?.length) { if (!pinList?.tags?.length) {
setPinEvents([]) setPinEvents([])
@ -112,13 +109,19 @@ export function useProfilePins(pubkey: string | undefined) {
return return
} }
const eventIds = pinList.tags.filter((tag) => tag[0] === 'e' && tag[1]).map((tag) => tag[1]) const max = PROFILE_PAGE_PINS_RESOLVE_LIMIT
const aTags = pinList.tags.filter((tag) => tag[0] === 'a' && tag[1]).map((tag) => tag[1]) 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<Event[]>[] = [] const eventPromises: Promise<Event[]>[] = []
if (eventIds.length > 0) { if (eventIds.length > 0) {
eventPromises.push( eventPromises.push(
queryService.fetchEvents(comprehensiveRelays, { ids: eventIds, limit: 100 }) queryService.fetchEvents(profileRelays, { ids: eventIds, limit: max })
) )
} }
if (aTags.length > 0) { if (aTags.length > 0) {
@ -131,7 +134,7 @@ export function useProfilePins(pubkey: string | undefined) {
const filter = d const filter = d
? { authors: [author], kinds: [kind], limit: 1, '#d': [d] as [string] } ? { authors: [author], kinds: [kind], limit: 1, '#d': [d] as [string] }
: { authors: [author], kinds: [kind], limit: 1 } : { authors: [author], kinds: [kind], limit: 1 }
const events = await queryService.fetchEvents(comprehensiveRelays, [filter]) const events = await queryService.fetchEvents(profileRelays, [filter])
return events[0] || null return events[0] || null
}) })
eventPromises.push( eventPromises.push(
@ -158,7 +161,7 @@ export function useProfilePins(pubkey: string | undefined) {
setLoadingPins(false) setLoadingPins(false)
} }
}, },
[pubkey, buildComprehensiveRelayList] [pubkey, favoriteRelays, blockedRelays]
) )
useEffect(() => { useEffect(() => {
@ -166,8 +169,7 @@ export function useProfilePins(pubkey: string | undefined) {
setPinEvents([]) setPinEvents([])
return return
} }
const t = setTimeout(() => void loadPins(false), 200) void loadPins(false)
return () => clearTimeout(t)
}, [pubkey, loadPins]) }, [pubkey, loadPins])
const refreshPins = useCallback(() => { const refreshPins = useCallback(() => {

35
src/hooks/useProfileTimeline.tsx

@ -3,9 +3,8 @@ import client from '@/services/client.service'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { CALENDAR_EVENT_KINDS, ExtendedKind } from '@/constants' 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 { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
type ProfileTimelineMemoryEntry = { type ProfileTimelineMemoryEntry = {
events: Event[] events: Event[]
@ -91,7 +90,6 @@ export function useProfileTimeline({
filterPredicate filterPredicate
}: UseProfileTimelineOptions): UseProfileTimelineResult { }: UseProfileTimelineOptions): UseProfileTimelineResult {
const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { relayList } = useNostr()
const { isEventDeleted, tombstoneEpoch } = useDeletedEvent() const { isEventDeleted, tombstoneEpoch } = useDeletedEvent()
const isEventDeletedRef = useRef(isEventDeleted) const isEventDeletedRef = useRef(isEventDeleted)
isEventDeletedRef.current = isEventDeleted isEventDeletedRef.current = isEventDeleted
@ -105,21 +103,8 @@ export function useProfileTimeline({
const [events, setEvents] = useState<Event[]>(cachedEntry?.events ?? []) const [events, setEvents] = useState<Event[]>(cachedEntry?.events ?? [])
const [isLoading, setIsLoading] = useState(!cachedEntry) const [isLoading, setIsLoading] = useState(!cachedEntry)
const [refreshToken, setRefreshToken] = useState(0) const [refreshToken, setRefreshToken] = useState(0)
const [authorOutboxWrite, setAuthorOutboxWrite] = useState<string[]>([])
const subscriptionRef = useRef<() => void>(() => {}) 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(() => { useEffect(() => {
setEvents((prev) => { setEvents((prev) => {
const next = prev.filter((e) => !isEventDeletedRef.current(e)) 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 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, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], authorRl,
{ kinds.includes(1)
userWriteRelays: relayList?.write ?? [],
authorWriteRelays: authorOutboxWrite,
applyKind1BlockedFilter: kinds.includes(1)
}
) )
const startWave = async (subRequests: ReturnType<typeof buildSubRequests>) => { const startWave = async (subRequests: ReturnType<typeof buildSubRequests>) => {
@ -239,9 +224,7 @@ export function useProfileTimeline({
filterPredicate, filterPredicate,
refreshToken, refreshToken,
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays
relayList,
authorOutboxWrite
]) ])
const refresh = useCallback(() => { const refresh = useCallback(() => {

24
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) * 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); * kind-1-blocked stripped, deduped, capped. Subrequest `urls` are prepended first by default (following shards);

Loading…
Cancel
Save