+ {relayWavePendingBannerEl}
{feedClientFilterActive && filteredEvents.length > 0 && clientFilteredEvents.length === 0 ? (
{t('No loaded posts match your filters.')}
diff --git a/src/constants.ts b/src/constants.ts
index 8c927466..73bfe032 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -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
diff --git a/src/hooks/useProfileAccordionData.tsx b/src/hooks/useProfileAccordionData.tsx
index b2faa917..25ed9dc7 100644
--- a/src/hooks/useProfileAccordionData.tsx
+++ b/src/hooks/useProfileAccordionData.tsx
@@ -1,5 +1,6 @@
import {
fetchProfileAccordionBundle,
+ mergeProfileAccordionBundles,
profileAccordionBundleCacheKey,
type ProfileAccordionBundle
} from '@/lib/profile-accordion-fetch'
@@ -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: {
const [data, setData] = useState
(EMPTY)
const [loading, setLoading] = useState(false)
const reqId = useRef(0)
+ const lastSuccessfulRelayUrlsRef = useRef([])
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: {
})
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: {
[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: {
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,
diff --git a/src/hooks/useProfileRelayUrls.tsx b/src/hooks/useProfileRelayUrls.tsx
index ed2b1e26..b2e8ad59 100644
--- a/src/hooks/useProfileRelayUrls.tsx
+++ b/src/hooks/useProfileRelayUrls.tsx
@@ -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
}
}
+ 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 {
diff --git a/src/hooks/useProfileTimeline.tsx b/src/hooks/useProfileTimeline.tsx
index 43c2a19e..bc1ff1c8 100644
--- a/src/hooks/useProfileTimeline.tsx
+++ b/src/hooks/useProfileTimeline.tsx
@@ -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({
}
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) => {
@@ -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()
diff --git a/src/hooks/useQuoteEvents.tsx b/src/hooks/useQuoteEvents.tsx
index e2ed484d..354393dd 100644
--- a/src/hooks/useQuoteEvents.tsx
+++ b/src/hooks/useQuoteEvents.tsx
@@ -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) {
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 }
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 9e17bba0..c46dc431 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -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)':
diff --git a/src/lib/profile-accordion-fetch.ts b/src/lib/profile-accordion-fetch.ts
index f35ff96c..4875c612 100644
--- a/src/lib/profile-accordion-fetch.ts
+++ b/src/lib/profile-accordion-fetch.ts
@@ -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()
+ 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 }
+}
diff --git a/src/lib/profile-relay-urls.ts b/src/lib/profile-relay-urls.ts
index a42165f8..77ba3788 100644
--- a/src/lib/profile-relay-urls.ts
+++ b/src/lib/profile-relay-urls.ts
@@ -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()
+ 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[] = []
diff --git a/src/lib/url.ts b/src/lib/url.ts
index ab5452ae..54b73723 100644
--- a/src/lib/url.ts
+++ b/src/lib/url.ts
@@ -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()
+ 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
+}
diff --git a/src/pages/primary/NoteListPage/FollowingFeed.tsx b/src/pages/primary/NoteListPage/FollowingFeed.tsx
index a75e1925..19741ed8 100644
--- a/src/pages/primary/NoteListPage/FollowingFeed.tsx
+++ b/src/pages/primary/NoteListPage/FollowingFeed.tsx
@@ -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<
})
}
+ 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<
{
if (relayUrls.length === 0) {
- setRelayAlgoReady(false)
+ setAreAlgoRelays(false)
return
}
let cancelled = false
- setRelayAlgoReady(false)
const init = async () => {
const timeoutPromise = new Promise((_, reject) => {
@@ -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<
ref={ref}
subRequests={subRequests}
areAlgoRelays={areAlgoRelays}
- relayCapabilityReady={relayAlgoReady}
isMainFeed
setSubHeader={setSubHeader}
onSubHeaderRefresh={onSubHeaderRefresh}
diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx
index fa21311c..5d67963a 100644
--- a/src/pages/primary/SpellsPage/index.tsx
+++ b/src/pages/primary/SpellsPage/index.tsx
@@ -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(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(function SpellsPage(
}, [spellProp, logSpellFeedPickerSelection])
const [followingSubRequests, setFollowingSubRequests] = useState([])
- const [followingFeedLoading, setFollowingFeedLoading] = useState(false)
const [favoritesSubRequests, setFavoritesSubRequests] = useState([])
- const [favoritesFeedLoading, setFavoritesFeedLoading] = useState(false)
const loadSpells = useCallback(async () => {
const [events, ids] = await Promise.all([
@@ -733,7 +740,6 @@ const SpellsPage = forwardRef(function SpellsPage(
useEffect(() => {
if (!pubkey || !isFollowFeedFauxSpellId(selectedFauxSpell)) {
setFollowingSubRequests([])
- setFollowingFeedLoading(false)
return
}
@@ -744,18 +750,46 @@ const SpellsPage = forwardRef(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(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(function SpellsPage(
sortedBlockedRelaysKey,
relayMailboxStableKey,
followSetCatalogLoading,
- followSetListStableKey
+ followSetListStableKey,
+ followListEvent?.id
])
const favoritesShowKinds = useMemo(() => {
@@ -810,12 +834,10 @@ const SpellsPage = forwardRef(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(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([pubkey, ...contacts])
for (const ev of followSetListEvents) {
if (ev.pubkey !== pubkey) continue
@@ -851,13 +906,7 @@ const SpellsPage = forwardRef(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(function SpellsPage(
}
} catch {
if (!cancelled) setFavoritesSubRequests([])
- } finally {
- if (!cancelled) setFavoritesFeedLoading(false)
}
})()
@@ -1337,13 +1384,11 @@ const SpellsPage = forwardRef(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(function SpellsPage(
{t('Please login to view favorites')}
- ) : showAsyncFauxFeedLoading ? (
- {t('loading...')}
) : selectedFauxSpell && fauxSubRequests.length === 0 ? (
{fauxFeedEmptyMessage}
) : selectedFauxSpell && fauxSubRequests.length > 0 ? (
@@ -1808,6 +1851,8 @@ const SpellsPage = forwardRef(function SpellsPage(
subRequests={subRequests}
feedSubscriptionKey={spellFeedSubscriptionKey}
hostPrimaryPageName="spells"
+ preserveTimelineOnSubRequestsChange={spellFauxMergeTimeline}
+ mergeTimelineWhenSubRequestFiltersMatch={spellFauxMergeTimeline}
showKinds={
selectedFauxSpell === 'notifications' ? NOTIFICATION_SPELL_KINDS : showKinds
}