Browse Source

speed up feeds

imwald
Silberengel 4 weeks ago
parent
commit
ebb7b4ad60
  1. 21
      src/components/NoteList/index.tsx
  2. 2
      src/constants.ts
  3. 77
      src/hooks/useProfileAccordionData.tsx
  4. 11
      src/hooks/useProfileRelayUrls.tsx
  5. 37
      src/hooks/useProfileTimeline.tsx
  6. 3
      src/hooks/useQuoteEvents.tsx
  7. 1
      src/i18n/locales/en.ts
  8. 55
      src/lib/profile-accordion-fetch.ts
  9. 18
      src/lib/profile-relay-urls.ts
  10. 18
      src/lib/url.ts
  11. 43
      src/pages/primary/NoteListPage/FollowingFeed.tsx
  12. 7
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  13. 133
      src/pages/primary/SpellsPage/index.tsx

21
src/components/NoteList/index.tsx

@ -1531,6 +1531,9 @@ const NoteList = forwardRef( @@ -1531,6 +1531,9 @@ const NoteList = forwardRef(
? ALGO_LIMIT
: LIMIT
// New REQ wave (incl. delta relays with same feed key): outcomes stay stale until this wave ends.
setFeedSubscribeRelayOutcomes([])
timelineSubscribePromise = client.subscribeTimeline(
mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }>,
{
@ -2528,6 +2531,23 @@ const NoteList = forwardRef( @@ -2528,6 +2531,23 @@ const NoteList = forwardRef(
const listSourceEvents = timelineEventsForFilter
const feedFullSearchActive = feedFullSearchEvents !== null
const showRelaySubscribeWavePendingBanner =
!oneShotFetch &&
!feedFullSearchActive &&
subRequests.length > 0 &&
relayCapabilityReady &&
timelineKey != null &&
feedSubscribeRelayOutcomes.length === 0 &&
feedTimelineEmptyUiReady
const relayWavePendingBannerEl = showRelaySubscribeWavePendingBanner ? (
<div
className="mb-2 rounded border border-border/40 bg-muted/15 px-3 py-1.5 text-center text-xs text-muted-foreground"
role="status"
aria-live="polite"
>
{t('Looking for more events…')}
</div>
) : null
const eventReasonLabelMap = useMemo(() => {
const reqs = subRequestsRef.current.filter((req) => req.reasonLabel && req.reasonLabel.trim().length > 0)
if (!reqs.length || !clientFilteredEvents.length) return new Map<string, string>()
@ -2548,6 +2568,7 @@ const NoteList = forwardRef( @@ -2548,6 +2568,7 @@ const NoteList = forwardRef(
const list = (
<div className="min-h-screen">
{relayWavePendingBannerEl}
{feedClientFilterActive && filteredEvents.length > 0 && clientFilteredEvents.length === 0 ? (
<div className="px-2 py-8 text-center text-sm text-muted-foreground">
{t('No loaded posts match your filters.')}

2
src/constants.ts

@ -84,7 +84,7 @@ export const RELAY_READ_ONLY_POOL_CONNECT_TIMEOUT_MS = 45_000 @@ -84,7 +84,7 @@ export const RELAY_READ_ONLY_POOL_CONNECT_TIMEOUT_MS = 45_000
export const RELAY_NIP42_PUBLISH_ACK_TIMEOUT_MS = 90_000
/** Multi-relay queries and timeline initial REQ: after the first event, wait this long then close (query) or finalize EOSE (live feed) while keeping the subscription open for new events. */
export const FIRST_RELAY_RESULT_GRACE_MS = 5000
export const FIRST_RELAY_RESULT_GRACE_MS = 2000
/** Legacy name: was used to cap spell NoteList skeleton time; loading now ends on EOSE / first events / safety timeouts. Kept for forks. */
export const SPELL_FEED_LOADING_MAX_MS = 1000

77
src/hooks/useProfileAccordionData.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import {
fetchProfileAccordionBundle,
mergeProfileAccordionBundles,
profileAccordionBundleCacheKey,
type ProfileAccordionBundle
} from '@/lib/profile-accordion-fetch'
@ -7,10 +8,16 @@ import { @@ -7,10 +8,16 @@ import {
profileAccordionGetCachedBadges,
profileAccordionGetCachedFollowPacks,
profileAccordionGetCachedInteractions,
profileAccordionGetCachedReports
profileAccordionGetCachedReports,
profileAccordionRelayUrlsKey,
profileAccordionSetBadges,
profileAccordionSetFollowPacks,
profileAccordionSetInteractions,
profileAccordionSetReports
} from '@/lib/profile-accordion-session-cache'
import { subtractNormalizedRelayUrls } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
const EMPTY: ProfileAccordionBundle = {
zaps: [],
@ -59,12 +66,17 @@ export function useProfileAccordionData(opts: { @@ -59,12 +66,17 @@ export function useProfileAccordionData(opts: {
const [data, setData] = useState<ProfileAccordionBundle>(EMPTY)
const [loading, setLoading] = useState(false)
const reqId = useRef(0)
const lastSuccessfulRelayUrlsRef = useRef<string[]>([])
const relayKey = useMemo(
() => profileAccordionBundleCacheKey(relayUrls ?? []),
[relayUrls]
)
useEffect(() => {
lastSuccessfulRelayUrlsRef.current = []
}, [pubkey])
const runFetch = useCallback(
async (force: boolean, overrideUrls?: string[]) => {
const urls = (overrideUrls?.length ? overrideUrls : relayUrls) ?? []
@ -86,6 +98,7 @@ export function useProfileAccordionData(opts: { @@ -86,6 +98,7 @@ export function useProfileAccordionData(opts: {
})
if (id !== reqId.current) return
setData(bundle)
lastSuccessfulRelayUrlsRef.current = urls
} finally {
if (id === reqId.current) setLoading(false)
}
@ -93,6 +106,46 @@ export function useProfileAccordionData(opts: { @@ -93,6 +106,46 @@ export function useProfileAccordionData(opts: {
[pubkey, relayUrls, viewerPubkey, favoriteRelays, blockedRelays]
)
const runMergeFetch = useCallback(
async (fullRelayUrls: string[], deltaUrls: string[], base: ProfileAccordionBundle) => {
const pk = pubkey?.trim()
if (!pk || !deltaUrls.length) return
const id = ++reqId.current
setLoading(true)
try {
const deltaB = await fetchProfileAccordionBundle({
pubkey: pk,
urls: deltaUrls,
viewerPubkey,
favoriteRelays: favoriteRelays ?? [],
blockedRelays,
force: true,
onPartial: (partial) => {
if (id !== reqId.current) return
setData(mergeProfileAccordionBundles(base, partial))
}
})
if (id !== reqId.current) return
const merged = mergeProfileAccordionBundles(base, deltaB)
setData(merged)
const fullKey = profileAccordionBundleCacheKey(fullRelayUrls)
profileAccordionSetInteractions(pk, fullKey, {
zaps: merged.zaps,
reactions: merged.reactions,
comments: merged.comments
})
profileAccordionSetBadges(pk, fullKey, merged.badges)
profileAccordionSetFollowPacks(pk, fullKey, merged.followPacks)
const viewer = viewerPubkey?.trim()
if (viewer) profileAccordionSetReports(pk, viewer, merged.reports)
lastSuccessfulRelayUrlsRef.current = fullRelayUrls
} finally {
if (id === reqId.current) setLoading(false)
}
},
[pubkey, viewerPubkey, favoriteRelays, blockedRelays]
)
const refresh = useCallback(
(overrideUrls?: string[]) => {
void runFetch(true, overrideUrls)
@ -109,11 +162,29 @@ export function useProfileAccordionData(opts: { @@ -109,11 +162,29 @@ export function useProfileAccordionData(opts: {
if (cached) {
setData(cached)
setLoading(false)
lastSuccessfulRelayUrlsRef.current = relayUrls
return
}
const prevSucc = lastSuccessfulRelayUrlsRef.current
if (
prevSucc.length > 0 &&
profileAccordionRelayUrlsKey(prevSucc) !== profileAccordionRelayUrlsKey(relayUrls)
) {
const delta = subtractNormalizedRelayUrls(relayUrls, prevSucc)
if (delta.length > 0) {
const prevKey = profileAccordionBundleCacheKey(prevSucc)
const base = readFullCache(pk, prevKey, viewerPubkey)
if (base) {
void runMergeFetch(relayUrls, delta, base)
return
}
}
}
setLoading(true)
void runFetch(false)
}, [enabled, pubkey, relayKey, relayUrls, viewerPubkey, runFetch])
}, [enabled, pubkey, relayKey, relayUrls, viewerPubkey, runFetch, runMergeFetch])
return {
...data,

11
src/hooks/useProfileRelayUrls.tsx

@ -3,7 +3,7 @@ import { @@ -3,7 +3,7 @@ import {
profileAccordionRelayUrlsKey,
profileAccordionSetRelayUrls
} from '@/lib/profile-accordion-session-cache'
import { buildProfileRelayUrls } from '@/lib/profile-relay-urls'
import { buildProfileRelayUrls, getProfileRelayUrlsProvisional } from '@/lib/profile-relay-urls'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -37,8 +37,17 @@ export function useProfileRelayUrls(pubkey: string | undefined, enabled: boolean @@ -37,8 +37,17 @@ export function useProfileRelayUrls(pubkey: string | undefined, enabled: boolean
}
}
const provisional = getProfileRelayUrlsProvisional(blockedRelaysRef.current)
const revalidateWithVisibleUrls = force && relayUrlsRef.current.length > 0
if (!revalidateWithVisibleUrls) {
if (provisional.length > 0) {
profileAccordionSetRelayUrls(pubkey, provisional)
setRelayUrls(provisional)
setLoading(false)
} else {
setLoading(true)
}
} else {
setLoading(true)
}
try {

37
src/hooks/useProfileTimeline.tsx

@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event } from 'nostr-tools'
import { CALENDAR_EVENT_KINDS, ExtendedKind, isSocialKindBlockedKind } from '@/constants'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
import { normalizeUrl } from '@/lib/url'
import { normalizeUrl, subtractNormalizedRelayUrls } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
type ProfileTimelineMemoryEntry = {
@ -202,15 +202,13 @@ export function useProfileTimeline({ @@ -202,15 +202,13 @@ export function useProfileTimeline({
}
const hasCalendarKinds = kinds.some((k) => CALENDAR_EVENT_KINDS.includes(k))
const authorRl = await client.fetchRelayList(pubkey).catch(() => ({
read: [] as string[],
write: [] as string[]
}))
const feedRelayUrls = buildProfilePageReadRelayUrls(
const socialKinds = kinds.some(isSocialKindBlockedKind)
const emptyAuthor = { read: [] as string[], write: [] as string[] }
const provisionalFeedUrls = buildProfilePageReadRelayUrls(
favoriteRelays,
blockedRelays,
authorRl,
kinds.some(isSocialKindBlockedKind)
emptyAuthor,
socialKinds
)
const startWave = async (subRequests: ReturnType<typeof buildSubRequests>) => {
@ -240,12 +238,31 @@ export function useProfileTimeline({ @@ -240,12 +238,31 @@ export function useProfileTimeline({
}
}
if (feedRelayUrls.length === 0) {
if (provisionalFeedUrls.length === 0) {
if (!cancelled) setIsLoading(false)
return
}
void startWave(buildSubRequests([feedRelayUrls], pubkey, kinds, limit, hasCalendarKinds))
void startWave(
buildSubRequests([provisionalFeedUrls], pubkey, kinds, limit, hasCalendarKinds)
)
void (async () => {
const authorRl = await client.fetchRelayList(pubkey).catch(() => ({
read: [] as string[],
write: [] as string[]
}))
if (cancelled) return
const fullFeedUrls = buildProfilePageReadRelayUrls(
favoriteRelays,
blockedRelays,
authorRl,
socialKinds
)
const deltaUrls = subtractNormalizedRelayUrls(fullFeedUrls, provisionalFeedUrls)
if (cancelled || deltaUrls.length === 0) return
await startWave(buildSubRequests([deltaUrls], pubkey, kinds, limit, hasCalendarKinds))
})()
}
void subscribe()

3
src/hooks/useQuoteEvents.tsx

@ -11,6 +11,7 @@ import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' @@ -11,6 +11,7 @@ import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import type { TSubRequestFilter } from '@/types'
import dayjs from 'dayjs'
import { Event, kinds } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
@ -99,7 +100,7 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) { @@ -99,7 +100,7 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) {
const highlightKinds = [kinds.Highlights] as const
const otherBacklinkKinds = [...THREAD_BACKLINK_STREAM_KINDS_WITHOUT_HIGHLIGHT]
const subRequests: { urls: string[]; filter: Filter }[] = [
const subRequests: { urls: string[]; filter: TSubRequestFilter }[] = [
{
urls: finalRelayUrls,
filter: { '#q': [qeIdForTagFilter], kinds: [kinds.ShortTextNote], limit: LIMIT }

1
src/i18n/locales/en.ts

@ -813,6 +813,7 @@ export default { @@ -813,6 +813,7 @@ export default {
'Nothing to load for this feed.': 'Nothing to load for this feed.',
'No posts loaded for this feed. Try refreshing.':
'No posts loaded for this feed. Try refreshing.',
'Looking for more events…': 'Looking for more events…',
'Relays returned no events for this feed. They may be offline, slow, or not indexing these notes.':
'Relays returned no events for this feed. They may be offline, slow, or not indexing these notes.',
'Per-relay timeline results ({{count}} connections)':

55
src/lib/profile-accordion-fetch.ts

@ -365,3 +365,58 @@ export async function fetchProfileAccordionBundle(args: { @@ -365,3 +365,58 @@ export async function fetchProfileAccordionBundle(args: {
export function profileAccordionBundleCacheKey(urls: string[]): string {
return profileAccordionRelayUrlsKey(urls)
}
function badgeMergeKey(b: TProfileBadge): string {
return `${b.a}|${b.awardId}`
}
/** Merge two accordion bundles (e.g. provisional relays + delta-only second fetch). */
export function mergeProfileAccordionBundles(
base: ProfileAccordionBundle,
add: ProfileAccordionBundle
): ProfileAccordionBundle {
const zapByPr = new Map(base.zaps.map((z) => [z.pr, z]))
for (const z of add.zaps) {
if (!zapByPr.has(z.pr)) zapByPr.set(z.pr, z)
}
const zaps = [...zapByPr.values()].sort((a, b) => b.amount - a.amount)
const reactionsByPubkey = new Map<string, Event>()
for (const e of base.reactions) {
reactionsByPubkey.set(e.pubkey, e)
}
for (const e of add.reactions) {
const prev = reactionsByPubkey.get(e.pubkey)
if (!prev || e.created_at > prev.created_at) reactionsByPubkey.set(e.pubkey, e)
}
const reactions = [...reactionsByPubkey.values()].sort((a, b) => b.created_at - a.created_at)
const commentById = new Map(base.comments.map((c) => [c.id, c]))
for (const c of add.comments) {
if (!commentById.has(c.id)) commentById.set(c.id, c)
}
const comments = [...commentById.values()].sort((a, b) => b.created_at - a.created_at)
const packByKey = new Map(base.followPacks.map((p) => [replaceableEventDedupeKey(p.event), p]))
for (const p of add.followPacks) {
const k = replaceableEventDedupeKey(p.event)
const prev = packByKey.get(k)
if (!prev || p.event.created_at > prev.event.created_at) packByKey.set(k, p)
}
const followPacks = [...packByKey.values()].sort((a, b) => b.event.created_at - a.event.created_at)
const badgeByKey = new Map(base.badges.map((b) => [badgeMergeKey(b), b]))
for (const b of add.badges) {
const k = badgeMergeKey(b)
if (!badgeByKey.has(k)) badgeByKey.set(k, b)
}
const badges = [...badgeByKey.values()]
const reportById = new Map(base.reports.map((r) => [r.id, r]))
for (const r of add.reports) {
if (!reportById.has(r.id)) reportById.set(r.id, r)
}
const reports = [...reportById.values()].sort((a, b) => b.created_at - a.created_at)
return { zaps, reactions, comments, badges, followPacks, reports }
}

18
src/lib/profile-relay-urls.ts

@ -7,6 +7,24 @@ import { E_TAG_FILTER_BLOCKED_RELAY_URLS, PROFILE_FETCH_RELAY_URLS } from '@/con @@ -7,6 +7,24 @@ import { E_TAG_FILTER_BLOCKED_RELAY_URLS, PROFILE_FETCH_RELAY_URLS } from '@/con
import client from '@/services/client.service'
import { normalizeUrl } from '@/lib/url'
/**
* Immediate relay stack before NIP-65 outboxes resolve (accordion / fast first paint).
*/
export function getProfileRelayUrlsProvisional(blockedRelays: string[] = []): string[] {
const blocked = new Set(
[...blockedRelays, ...E_TAG_FILTER_BLOCKED_RELAY_URLS].map((u) => (normalizeUrl(u) || u).toLowerCase())
)
const out: string[] = []
const seen = new Set<string>()
for (const u of PROFILE_FETCH_RELAY_URLS) {
const n = normalizeUrl(u) || u
if (!n || blocked.has(n.toLowerCase()) || seen.has(n)) continue
seen.add(n)
out.push(n)
}
return out
}
export async function buildProfileRelayUrls(
pubkey: string,
blockedRelays: string[] = []

18
src/lib/url.ts

@ -441,3 +441,21 @@ export function rewritePlainTextHttpUrls( @@ -441,3 +441,21 @@ export function rewritePlainTextHttpUrls(
}
})
}
/**
* Relays in `full` whose normalized URL is not in `provisional` (by {@link normalizeUrl}), preserving first-seen order.
*/
export function subtractNormalizedRelayUrls(full: string[], provisional: string[]): string[] {
const prov = new Set(
provisional.map((u) => normalizeUrl(u) || u.trim()).filter(Boolean)
)
const seen = new Set<string>()
const out: string[] = []
for (const u of full) {
const n = normalizeUrl(u) || u.trim()
if (!n || prov.has(n) || seen.has(n)) continue
seen.add(n)
out.push(u)
}
return out
}

43
src/pages/primary/NoteListPage/FollowingFeed.tsx

@ -69,11 +69,29 @@ const FollowingFeed = forwardRef< @@ -69,11 +69,29 @@ const FollowingFeed = forwardRef<
return
}
let followings: string[] = []
const augment = (raw: TFeedSubRequest[]) =>
augmentSubRequestsWithFavoritesFastReadAndInbox(
raw,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] }
)
const fromTags = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
const provisionalAuthors = [...new Set([pubkey, ...fromTags])]
try {
const rawProv = await client.generateSubRequestsForPubkeys(provisionalAuthors, pubkey)
if (!cancelled) setSubRequests(augment(rawProv))
} catch (error) {
logger.warn('[FollowingFeed] provisional generateSubRequestsForPubkeys failed', { error })
}
let followings: string[] = fromTags
try {
followings = await client.fetchFollowings(pubkey)
} catch (error) {
// Failsafe: keep follows feed usable when contacts fetch relay calls fail transiently.
followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
logger.warn('[FollowingFeed] fetchFollowings failed; using cached follow list fallback', {
error,
@ -81,16 +99,17 @@ const FollowingFeed = forwardRef< @@ -81,16 +99,17 @@ const FollowingFeed = forwardRef<
})
}
const fullAuthors = [...new Set([pubkey, ...followings])]
const sameSize = fullAuthors.length === provisionalAuthors.length
const sameSet =
sameSize && fullAuthors.every((p) => provisionalAuthors.includes(p)) && provisionalAuthors.every((p) => fullAuthors.includes(p))
if (sameSet) {
return
}
try {
const raw = await client.generateSubRequestsForPubkeys([pubkey, ...followings], pubkey)
const augmented = augmentSubRequestsWithFavoritesFastReadAndInbox(
raw,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] }
)
if (!cancelled) setSubRequests(augmented)
const raw = await client.generateSubRequestsForPubkeys(fullAuthors, pubkey)
if (!cancelled) setSubRequests(augment(raw))
} catch (error) {
logger.error('[FollowingFeed] generateSubRequestsForPubkeys failed', error)
if (!cancelled) setSubRequests([])
@ -115,6 +134,8 @@ const FollowingFeed = forwardRef< @@ -115,6 +134,8 @@ const FollowingFeed = forwardRef<
<NormalFeed
ref={ref}
subRequests={subRequests}
preserveTimelineOnSubRequestsChange
mergeTimelineWhenSubRequestFiltersMatch
isMainFeed
setSubHeader={setSubHeader}
onSubHeaderRefresh={onSubHeaderRefresh}

7
src/pages/primary/NoteListPage/RelaysFeed.tsx

@ -23,7 +23,6 @@ const RelaysFeed = forwardRef< @@ -23,7 +23,6 @@ const RelaysFeed = forwardRef<
const { feedInfo, relayUrls } = useFeed()
const { showKinds } = useKindFilterOrDefaults()
const [areAlgoRelays, setAreAlgoRelays] = useState(false)
const [relayAlgoReady, setRelayAlgoReady] = useState(false)
/** After kindless single-relay REQ EOSEs with no events, re-subscribe with the normal kind list. */
const [singleRelayKindFallback, setSingleRelayKindFallback] = useState(false)
@ -39,11 +38,10 @@ const RelaysFeed = forwardRef< @@ -39,11 +38,10 @@ const RelaysFeed = forwardRef<
useEffect(() => {
if (relayUrls.length === 0) {
setRelayAlgoReady(false)
setAreAlgoRelays(false)
return
}
let cancelled = false
setRelayAlgoReady(false)
const init = async () => {
const timeoutPromise = new Promise<never>((_, reject) => {
@ -62,8 +60,6 @@ const RelaysFeed = forwardRef< @@ -62,8 +60,6 @@ const RelaysFeed = forwardRef<
setAreAlgoRelays(areAlgo)
} catch (_error) {
if (!cancelled) setAreAlgoRelays(false)
} finally {
if (!cancelled) setRelayAlgoReady(true)
}
}
@ -148,7 +144,6 @@ const RelaysFeed = forwardRef< @@ -148,7 +144,6 @@ const RelaysFeed = forwardRef<
ref={ref}
subRequests={subRequests}
areAlgoRelays={areAlgoRelays}
relayCapabilityReady={relayAlgoReady}
isMainFeed
setSubHeader={setSubHeader}
onSubHeaderRefresh={onSubHeaderRefresh}

133
src/pages/primary/SpellsPage/index.tsx

@ -51,6 +51,7 @@ import { @@ -51,6 +51,7 @@ import {
FIRST_RELAY_RESULT_GRACE_MS,
} from '@/constants'
import { filterEventsExcludingTombstones, isUserInEventMentions } from '@/lib/event'
import { getPubkeysFromPTags } from '@/lib/tag'
import { formatPubkey, normalizeHexPubkey } from '@/lib/pubkey'
import {
augmentSubRequestsWithFavoritesFastReadAndInbox,
@ -314,7 +315,15 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -314,7 +315,15 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
) {
const { t } = useTranslation()
const { navigate: navigatePrimary } = usePrimaryPage()
const { pubkey, account, relayList, attemptDelete, bookmarkListEvent, interestListEvent } = useNostr()
const {
pubkey,
account,
relayList,
attemptDelete,
bookmarkListEvent,
interestListEvent,
followListEvent
} = useNostr()
const { addBookmark, removeBookmark } = useBookmarks()
const { hideUntrustedNotifications } = useUserTrust()
const { isSmallScreen } = useScreenSize()
@ -399,9 +408,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -399,9 +408,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
}, [spellProp, logSpellFeedPickerSelection])
const [followingSubRequests, setFollowingSubRequests] = useState<TFeedSubRequest[]>([])
const [followingFeedLoading, setFollowingFeedLoading] = useState(false)
const [favoritesSubRequests, setFavoritesSubRequests] = useState<TFeedSubRequest[]>([])
const [favoritesFeedLoading, setFavoritesFeedLoading] = useState(false)
const loadSpells = useCallback(async () => {
const [events, ids] = await Promise.all([
@ -733,7 +740,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -733,7 +740,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
useEffect(() => {
if (!pubkey || !isFollowFeedFauxSpellId(selectedFauxSpell)) {
setFollowingSubRequests([])
setFollowingFeedLoading(false)
return
}
@ -744,18 +750,46 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -744,18 +750,46 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
if (followSetD && followSetCatalogLoading) {
setFollowingSubRequests([])
setFollowingFeedLoading(true)
return
}
let cancelled = false
setFollowingFeedLoading(true)
void (async () => {
const augment = (raw: TFeedSubRequest[]) =>
augmentSubRequestsWithFavoritesFastReadAndInbox(
raw,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] }
)
try {
let authorPubkeys: string[]
if (selectedFauxSpell === 'following') {
const followings = await client.fetchFollowings(pubkey)
authorPubkeys = [pubkey, ...followings]
const fromTags = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
const provisionalAuthors = [...new Set([pubkey, ...fromTags])]
try {
const rawProv = await client.generateSubRequestsForPubkeys(provisionalAuthors, pubkey)
if (!cancelled) setFollowingSubRequests(augment(rawProv))
} catch {
/* refined wave may still succeed */
}
let followings = fromTags
try {
followings = await client.fetchFollowings(pubkey)
} catch {
followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
}
const fullAuthors = [...new Set([pubkey, ...followings])]
const sameSet =
fullAuthors.length === provisionalAuthors.length &&
fullAuthors.every((p) => provisionalAuthors.includes(p)) &&
provisionalAuthors.every((p) => fullAuthors.includes(p))
if (sameSet) {
return
}
const req = await client.generateSubRequestsForPubkeys(fullAuthors, pubkey)
if (!cancelled) setFollowingSubRequests(augment(req))
} else if (followSetD) {
const ev = followSetListEvents.find((e) => getFollowSetDTag(e) === followSetD)
if (!ev) {
@ -763,25 +797,14 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -763,25 +797,14 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
return
}
const listed = pubkeysFromFollowSetEvent(ev)
authorPubkeys = [pubkey, ...listed]
const authorPubkeys = [pubkey, ...listed]
const req = await client.generateSubRequestsForPubkeys(authorPubkeys, pubkey)
if (!cancelled) setFollowingSubRequests(augment(req))
} else {
if (!cancelled) setFollowingSubRequests([])
return
}
const req = await client.generateSubRequestsForPubkeys(authorPubkeys, pubkey)
const merged = augmentSubRequestsWithFavoritesFastReadAndInbox(
req,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] }
)
if (!cancelled) setFollowingSubRequests(merged)
} catch {
if (!cancelled) setFollowingSubRequests([])
} finally {
if (!cancelled) setFollowingFeedLoading(false)
}
})()
return () => {
@ -794,7 +817,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -794,7 +817,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
sortedBlockedRelaysKey,
relayMailboxStableKey,
followSetCatalogLoading,
followSetListStableKey
followSetListStableKey,
followListEvent?.id
])
const favoritesShowKinds = useMemo(() => {
@ -810,12 +834,10 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -810,12 +834,10 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
useEffect(() => {
if (selectedFauxSpell !== 'favorites' || !pubkey) {
setFavoritesSubRequests([])
setFavoritesFeedLoading(false)
return
}
let cancelled = false
setFavoritesFeedLoading(true)
void (async () => {
try {
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
@ -841,6 +863,39 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -841,6 +863,39 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
reasonLabel: t('Added from your web bookmarks')
}))
const augmentFollow = (raw: TFeedSubRequest[]) =>
augmentSubRequestsWithFavoritesFastReadAndInbox(
raw,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] }
).map((r) => ({ ...r, reasonLabel: t('Added from follows and contact lists') }))
const quickFollowRaw = await client.generateSubRequestsForPubkeys([pubkey], pubkey)
const quickFollowAug = augmentFollow(quickFollowRaw)
const followsWebQuick: TFeedSubRequest[] = [
{
urls: feedUrls,
filter: {
authors: [pubkey],
kinds: [ExtendedKind.WEB_BOOKMARK],
limit: FAUX_SPELL_EVENT_LIMIT
},
reasonLabel: t('Added from follows web bookmarks')
}
]
if (!cancelled) {
setFavoritesSubRequests([
...interestReqs,
...idReqs,
...ownWebReqs,
...followsWebQuick,
...quickFollowAug
])
}
const authorSet = new Set<string>([pubkey, ...contacts])
for (const ev of followSetListEvents) {
if (ev.pubkey !== pubkey) continue
@ -851,13 +906,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -851,13 +906,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const followAndContactReqs = authorPubkeys.length
? await client.generateSubRequestsForPubkeys(authorPubkeys, pubkey)
: []
const followAndContactAugmented = augmentSubRequestsWithFavoritesFastReadAndInbox(
followAndContactReqs,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] }
).map((r) => ({ ...r, reasonLabel: t('Added from follows and contact lists') }))
const followAndContactAugmented = augmentFollow(followAndContactReqs)
const followsWebBookmarkReqs: TFeedSubRequest[] = authorPubkeys.length
? [
@ -884,8 +933,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -884,8 +933,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
}
} catch {
if (!cancelled) setFavoritesSubRequests([])
} finally {
if (!cancelled) setFavoritesFeedLoading(false)
}
})()
@ -1337,13 +1384,11 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -1337,13 +1384,11 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
return t('Nothing to load for this feed.')
}, [selectedFauxSpell, fauxSubRequests.length, t])
const showAsyncFauxFeedLoading = !!(
pubkey &&
selectedFauxSpell &&
(selectedFauxSpell === 'favorites'
? favoritesFeedLoading
: isFollowFeedFauxSpellId(selectedFauxSpell) &&
(followingFeedLoading || (isFollowSetSpellId(selectedFauxSpell) && followSetCatalogLoading)))
const spellFauxMergeTimeline = useMemo(
() =>
selectedFauxSpell === 'favorites' ||
(!!selectedFauxSpell && isFollowFeedFauxSpellId(selectedFauxSpell)),
[selectedFauxSpell]
)
const spellStarAddTitle = t('Spell star add title')
@ -1788,8 +1833,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -1788,8 +1833,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
<div className="py-8 text-center text-muted-foreground">
{t('Please login to view favorites')}
</div>
) : showAsyncFauxFeedLoading ? (
<div className="py-8 text-center text-sm text-muted-foreground">{t('loading...')}</div>
) : selectedFauxSpell && fauxSubRequests.length === 0 ? (
<div className="py-8 text-center text-muted-foreground">{fauxFeedEmptyMessage}</div>
) : selectedFauxSpell && fauxSubRequests.length > 0 ? (
@ -1808,6 +1851,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -1808,6 +1851,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
subRequests={subRequests}
feedSubscriptionKey={spellFeedSubscriptionKey}
hostPrimaryPageName="spells"
preserveTimelineOnSubRequestsChange={spellFauxMergeTimeline}
mergeTimelineWhenSubRequestFiltersMatch={spellFauxMergeTimeline}
showKinds={
selectedFauxSpell === 'notifications' ? NOTIFICATION_SPELL_KINDS : showKinds
}

Loading…
Cancel
Save