diff --git a/nip66-cron/index.mjs b/nip66-cron/index.mjs index 66db5802..dc6c2ca0 100644 --- a/nip66-cron/index.mjs +++ b/nip66-cron/index.mjs @@ -60,7 +60,6 @@ const DEFAULT_RELAYS_TO_MONITOR = [ 'wss://relay.noswhere.com', 'wss://relay.wikifreedia.xyz', 'wss://nostr.einundzwanzig.space', - 'wss://relay.lumina.rocks', 'wss://nostrelites.org', 'wss://relay.nsec.app', 'wss://bucket.coracle.social', diff --git a/src/components/Explore/ExploreFavoriteRelays.tsx b/src/components/Explore/ExploreFavoriteRelays.tsx index a8b5d785..84dab479 100644 --- a/src/components/Explore/ExploreFavoriteRelays.tsx +++ b/src/components/Explore/ExploreFavoriteRelays.tsx @@ -2,12 +2,12 @@ import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '@/components/RelaySimp import { Button } from '@/components/ui/button' import { DEFAULT_FAVORITE_RELAYS } from '@/constants' import { useFetchRelayInfo } from '@/hooks' -import { toRelay } from '@/lib/link' +import { toRelay, toRelaySettings } from '@/lib/link' import { normalizeUrl, simplifyUrl } from '@/lib/url' -import { usePrimaryPage, useSmartRelayNavigation } from '@/PageManager' +import { usePrimaryPage, useSecondaryPage, useSmartRelayNavigation } from '@/PageManager' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { cn } from '@/lib/utils' -import { Newspaper } from 'lucide-react' +import { Newspaper, Settings } from 'lucide-react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -58,6 +58,7 @@ function FavoriteRelayCard({ url }: { url: string }) { export default function ExploreFavoriteRelays() { const { t } = useTranslation() const { navigate } = usePrimaryPage() + const { push } = useSecondaryPage() const { favoriteRelays, blockedRelays } = useFavoriteRelays() const blockedSet = useMemo( @@ -100,6 +101,17 @@ export default function ExploreFavoriteRelays() { {t('Favorites Feed')} + {usingDefaults ? ( {t('Using app default relays')} diff --git a/src/components/Explore/ExploreRelayReviews.tsx b/src/components/Explore/ExploreRelayReviews.tsx index be792bcb..6f99cf92 100644 --- a/src/components/Explore/ExploreRelayReviews.tsx +++ b/src/components/Explore/ExploreRelayReviews.tsx @@ -1,13 +1,30 @@ import NoteList from '@/components/NoteList' -import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' +import { ExtendedKind, PROFILE_FETCH_RELAY_URLS } from '@/constants' import { getRelayUrlFromRelayReviewEvent, getStarsFromRelayReviewEvent } from '@/lib/event-metadata' +import { buildExploreProfileAndUserRelayList } from '@/lib/relay-list-builder' +import { useNostr } from '@/providers/NostrProvider' import { Event } from 'nostr-tools' -import { useCallback } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' export default function ExploreRelayReviews() { + const { pubkey } = useNostr() + const [relayUrls, setRelayUrls] = useState(() => [...PROFILE_FETCH_RELAY_URLS]) + + useEffect(() => { + let cancelled = false + buildExploreProfileAndUserRelayList(pubkey ?? null).then((urls) => { + if (!cancelled) setRelayUrls(urls) + }) + return () => { + cancelled = true + } + }, [pubkey]) + + const subRequests = useMemo(() => [{ urls: relayUrls, filter: {} }], [relayUrls]) + const extraShouldHideEvent = useCallback((evt: Event) => { if (evt.kind !== ExtendedKind.RELAY_REVIEW) return false if (!getRelayUrlFromRelayReviewEvent(evt)) return true @@ -18,7 +35,7 @@ export default function ExploreRelayReviews() {
{} } + /** False after cleanup so stale timeline callbacks cannot overwrite state after switching feeds (e.g. Spells discussions → notifications). */ + let effectActive = true + async function init() { setLoading(true) setEvents([]) @@ -281,7 +284,10 @@ const NoteList = forwardRef( let closer: (() => void) | undefined let timelineKey: string | undefined - + let timelineSubscribePromise: + | Promise<{ closer: () => void; timelineKey: string }> + | undefined + try { // Add timeout wrapper to prevent subscribeTimeline from hanging indefinitely const timeoutPromise = new Promise((_, reject) => { @@ -290,11 +296,11 @@ const NoteList = forwardRef( }, 5000) // 5 second timeout }) - const result = await Promise.race([ - client.subscribeTimeline( + timelineSubscribePromise = client.subscribeTimeline( mappedSubRequests, { onEvents: (events: Event[], eosed: boolean) => { + if (!effectActive) return if (events.length > 0) { setEvents(events) // Do not wait for full EOSE across many relays — otherwise loading/skeleton stays up for 10–30s+ @@ -314,6 +320,7 @@ const NoteList = forwardRef( pubkeysToFetch.forEach((p) => prefetchedPubkeysRef.current.add(p)) // Batch fetch in background (non-blocking) with delay to not block initial render setTimeout(() => { + if (!effectActive) return client.fetchProfilesForPubkeys(pubkeysToFetch).catch(() => { // On error, remove from prefetched set so we can retry later pubkeysToFetch.forEach((p) => prefetchedPubkeysRef.current.delete(p)) @@ -337,6 +344,7 @@ const NoteList = forwardRef( eventIdsToFetch.forEach((id) => prefetchedEventIdsRef.current.add(id)) // Batch fetch embedded events in background (non-blocking) with delay setTimeout(() => { + if (!effectActive) return Promise.all(eventIdsToFetch.map((id) => client.fetchEvent(id))).catch(() => { // On error, remove from prefetched set so we can retry later eventIdsToFetch.forEach((id) => prefetchedEventIdsRef.current.delete(id)) @@ -369,6 +377,7 @@ const NoteList = forwardRef( } }, onNew: (event: Event) => { + if (!effectActive) return if (!useFilterAsIs && !showKinds.includes(event.kind)) return if (event.kind === kinds.ShortTextNote) { const isReply = isReplyNoteEvent(event) @@ -395,22 +404,34 @@ const NoteList = forwardRef( needSort: !areAlgoRelays, useCache: false // Main feeds should always fetch fresh from relays, not use cache } - ), - timeoutPromise - ]) + ) + + const result = await Promise.race([timelineSubscribePromise, timeoutPromise]) + if (!effectActive) { + result.closer() + return () => {} + } closer = result.closer timelineKey = result.timelineKey setTimelineKey(timelineKey) return closer } catch (_error) { setLoading(false) - // Return a no-op closer function instead of throwing - allows cleanup to work + // Race timeout or subscribe failure: if the timeline promise later resolves, close or subs leak (relay slots + stale setEvents). + if (timelineSubscribePromise) { + void timelineSubscribePromise + .then((r) => { + r.closer() + }) + .catch(() => {}) + } return () => {} } } const promise = init() return () => { + effectActive = false promise.then((closer) => closer?.()) } }, [ diff --git a/src/components/NoteStats/index.tsx b/src/components/NoteStats/index.tsx index 2413a4bf..47db1990 100644 --- a/src/components/NoteStats/index.tsx +++ b/src/components/NoteStats/index.tsx @@ -74,7 +74,8 @@ export default function NoteStats({ {displayTopZapsAndLikes && ( <> - + {/* Kind 11: LikeButton already shows ⬆️/⬇️; Likes row would duplicate those pills */} + {!isDiscussion && } )}
- + {!isDiscussion && } )}
diff --git a/src/constants.ts b/src/constants.ts index 32bbcd1f..fb694860 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -179,7 +179,6 @@ export const SEARCHABLE_RELAY_URLS = [ 'wss://relay.noswhere.com', 'wss://relay.wikifreedia.xyz', 'wss://nostr.einundzwanzig.space', - 'wss://relay.lumina.rocks', 'wss://nostrelites.org', 'wss://relay.nsec.app', 'wss://bucket.coracle.social', diff --git a/src/lib/relay-list-builder.ts b/src/lib/relay-list-builder.ts index 9c330c75..a9a1fdc6 100644 --- a/src/lib/relay-list-builder.ts +++ b/src/lib/relay-list-builder.ts @@ -230,6 +230,33 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio return Array.from(relayUrls) } +/** + * Explore: Following's Favorites (kind 10012 batch) and Relay reviews tab. + * PROFILE_FETCH_RELAY_URLS plus the viewer's read/write and cache (10432) relays — no FAST_READ. + */ +export async function buildExploreProfileAndUserRelayList( + userPubkey: string | null | undefined +): Promise { + if (!userPubkey) { + return Array.from(new Set([...PROFILE_FETCH_RELAY_URLS])) + } + try { + const built = await buildComprehensiveRelayList({ + userPubkey, + includeUserOwnRelays: true, + includeProfileFetchRelays: true, + includeFastReadRelays: false, + includeFavoriteRelays: false, + includeLocalRelays: true, + includeFastWriteRelays: false, + includeSearchableRelays: false + }) + return built.length > 0 ? built : Array.from(new Set([...PROFILE_FETCH_RELAY_URLS])) + } catch { + return Array.from(new Set([...PROFILE_FETCH_RELAY_URLS])) + } +} + /** * Build relay list for reading replies/comments * READ from: FAST_READ_RELAY_URLS + user's inboxes + local relays + OP author's outboxes diff --git a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts index 3da8819e..2f1c3090 100644 --- a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts +++ b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts @@ -17,6 +17,13 @@ const NOTIFICATION_LIMIT = 500 const DISCUSSION_LIMIT = 500 const MAX_BOOKMARK_IDS = 250 +/** + * Spells “Discussions” uses NoteList → subscribeTimeline → one live REQ per relay. + * The same merged list as DiscussionsPage’s one-shot query would open 80+ sockets and exhaust + * subscription slots; cap keeps first paint fast. Full coverage remains on /discussions. + */ +const DISCUSSION_FAUX_SPELL_MAX_RELAYS = 32 + export const MEDIA_SPELL_KINDS = [ ExtendedKind.PICTURE, ExtendedKind.VIDEO, @@ -66,7 +73,10 @@ export function buildMentionsSpellFilter(pubkey: string): Filter { } } -/** Relay set for discussion threads (kind 11), aligned with DiscussionsPage’s merged list (sync). */ +/** + * Relay set for Spells “Discussions” (kind 11): same merge order as DiscussionsPage, but capped + * for subscription-based loading (see DISCUSSION_FAUX_SPELL_MAX_RELAYS). + */ export function discussionRelayUrls( relayList: TRelayList | null | undefined, favoriteRelays: string[], @@ -83,6 +93,7 @@ export function discussionRelayUrls( if (!k || seen.has(k) || blocked.has(k)) continue seen.add(k) out.push(k) + if (out.length >= DISCUSSION_FAUX_SPELL_MAX_RELAYS) break } return out } diff --git a/src/services/client-replaceable-events.service.ts b/src/services/client-replaceable-events.service.ts index df8a0e66..b0c13785 100644 --- a/src/services/client-replaceable-events.service.ts +++ b/src/services/client-replaceable-events.service.ts @@ -18,7 +18,7 @@ import indexedDb from './indexed-db.service' import type { QueryService } from './client-query.service' import logger from '@/lib/logger' import client from './client.service' -import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' +import { buildComprehensiveRelayList, buildExploreProfileAndUserRelayList } from '@/lib/relay-list-builder' export class ReplaceableEventService { private queryService: QueryService @@ -436,6 +436,8 @@ export class ReplaceableEventService { // For metadata with a logged-in user, merge defaults with {@link buildComprehensiveRelayList}: inboxes (read), // local/cache relays (10432), favorite relays (10012), plus profile + fast read — same idea as favorites feed // / inbox-scoped discovery without per-author relay list fetches. + // Following's Favorites (Explore): kind 10012 batch uses PROFILE_FETCH_RELAY_URLS + viewer's own relays only + // (no FAST_READ), so outbox data is queried where the user actually reads + profile-index relays. let relayUrls: string[] if (kind === kinds.Metadata) { const userPk = client.pubkey @@ -457,6 +459,8 @@ export class ReplaceableEventService { } else { relayUrls = Array.from(new Set([...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS])) } + } else if (kind === ExtendedKind.FAVORITE_RELAYS) { + relayUrls = await buildExploreProfileAndUserRelayList(client.pubkey) } else { relayUrls = [...FAST_READ_RELAY_URLS] }