diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx index bb4d4516..c5779769 100644 --- a/src/components/NormalFeed/index.tsx +++ b/src/components/NormalFeed/index.tsx @@ -61,6 +61,11 @@ const NormalFeed = forwardRef number extraShouldHideEvent?: (ev: Event) => boolean extraShouldHideRepliesEvent?: (ev: Event) => boolean + /** When set with home Gallery, filters rows (e.g. aggr-only) using the widened relay stack. */ + extraShouldHideGalleryEvent?: (ev: Event) => boolean /** Override default cap for merged one-shot batches (wide d-tag / search merges). */ oneShotMergedCap?: number /** When every relay in the subscribe wave fails before EOSE, merge a one-shot fetch from default read relays (home multi-relay feeds). */ @@ -119,6 +126,7 @@ const NormalFeed = forwardRef { if (listMode === 'postsAndReplies' && repliesSubRequests) { return repliesSubRequests @@ -206,10 +215,29 @@ const NormalFeed = forwardRef ({ ...req, - urls: isMainFeed && widenMainGalleryRelays ? galleryRelayUrlsMergedWithReadLayer(req.urls) : req.urls, + urls: + isMainFeed && mainFeedGalleryRelayUrls && mainFeedGalleryRelayUrls.length > 0 + ? mainFeedGalleryRelayUrls + : isMainFeed && widenMainGalleryRelays + ? galleryRelayUrlsMergedWithReadLayer(req.urls) + : req.urls, filter: { ...req.filter, kinds: MEDIA_KINDS } })) - }, [listMode, subRequests, repliesSubRequests, MEDIA_KINDS, isMainFeed, widenMainGalleryRelays]) + }, [ + listMode, + subRequests, + repliesSubRequests, + MEDIA_KINDS, + isMainFeed, + widenMainGalleryRelays, + mainFeedGalleryRelayUrls + ]) + + const noteListExtraShouldHide = useMemo(() => { + if (listMode === 'postsAndReplies') return extraShouldHideRepliesEvent + if (listMode === 'media' && extraShouldHideGalleryEvent) return extraShouldHideGalleryEvent + return extraShouldHideEvent + }, [listMode, extraShouldHideRepliesEvent, extraShouldHideGalleryEvent, extraShouldHideEvent]) const handleListModeChange = useCallback( (mode: TNoteListMode | string) => { @@ -374,11 +402,7 @@ const NormalFeed = forwardRef !relayUrlIsNostrLandAggr(url)) +} + export function buildAllFavoritesFeedRelayUrls( favoriteRelays: string[], blockedRelays: string[], extraFeedRelayUrls: string[] ): string[] { - return feedRelayPolicyUrls([ - { source: 'favorites', urls: getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) }, - { source: 'fallback', urls: extraFeedRelayUrls } - ], { - operation: 'favorites-feed', - blockedRelays, - nostrLandAggr: 'never', - applySocialKindBlockedFilter: false, - allowThirdPartyLocalRelays: true - }).filter((url) => !relayUrlIsNostrLandAggr(url)) + return stripNostrLandAggrFromRelayUrls( + feedRelayPolicyUrls( + [ + { source: 'favorites', urls: getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) }, + { source: 'fallback', urls: extraFeedRelayUrls } + ], + { + operation: 'favorites-feed', + blockedRelays, + nostrLandAggr: 'never', + applySocialKindBlockedFilter: false, + allowThirdPartyLocalRelays: true + } + ) + ) } diff --git a/src/lib/nostr-land-relay-eligibility.ts b/src/lib/nostr-land-relay-eligibility.ts index dd5658c7..b2b29773 100644 --- a/src/lib/nostr-land-relay-eligibility.ts +++ b/src/lib/nostr-land-relay-eligibility.ts @@ -3,7 +3,8 @@ import { normalizeAnyRelayUrl } from '@/lib/url' /** * True when any URL’s host is `nostr.land` (e.g. `wss://nostr.land`, `wss://aggr.nostr.land`). - * Used to decide whether read fetches should prepend {@link AGGR_NOSTR_LAND_WSS} (except the primary home OP feed). + * Used to decide whether read fetches should prepend {@link AGGR_NOSTR_LAND_WSS} (home OP / Replies / Gallery + * never prepend aggr via {@link FeedProvider}; side-panel threads, profiles, spells, and other reads still use it). */ export function relayUrlsMentionNostrLandDomain(urls: readonly string[]): boolean { return urls.some((url) => { diff --git a/src/pages/primary/NoteListPage/RelaysFeed.tsx b/src/pages/primary/NoteListPage/RelaysFeed.tsx index 3c0e0faf..9ef460f1 100644 --- a/src/pages/primary/NoteListPage/RelaysFeed.tsx +++ b/src/pages/primary/NoteListPage/RelaysFeed.tsx @@ -115,6 +115,15 @@ const RelaysFeed = forwardRef< }, [relayUrls] ) + const hideAggrOnlyReplyGalleryStackEvent = useCallback( + (event: Event) => { + const seenRelays = client.getSeenEventRelayUrls(event.id).map(relaySeenKey) + if (!seenRelays.includes(AGGR_RELAY_KEY)) return false + const allowedRelays = new Set(replyRelayUrls.map(relaySeenKey)) + return !seenRelays.some((relay) => relay !== AGGR_RELAY_KEY && allowedRelays.has(relay)) + }, + [replyRelayUrls] + ) const hideAggrOnlyNonReplyEvent = useCallback( (event: Event) => hideAggrOnlyMainFeedEvent(event) && !isReplyNoteEvent(event), [hideAggrOnlyMainFeedEvent] @@ -135,12 +144,14 @@ const RelaysFeed = forwardRef< onSubHeaderRefresh={onSubHeaderRefresh} preserveTimelineOnSubRequestsChange repliesSubRequests={repliesSubRequests} + mainFeedGalleryRelayUrls={replyRelayUrls} widenMainGalleryRelays={false} feedSubscriptionKey="home-all-favorites" feedTimelineScopeKey="all-favorites" showFeedClientFilter hostPrimaryPageName="feed" extraShouldHideEvent={hideAggrOnlyMainFeedEvent} + extraShouldHideGalleryEvent={hideAggrOnlyReplyGalleryStackEvent} extraShouldHideRepliesEvent={hideAggrOnlyNonReplyEvent} timelinePublicReadFallback /> diff --git a/src/providers/FeedProvider.test.ts b/src/providers/FeedProvider.test.ts index e45ac7b3..390cd7e5 100644 --- a/src/providers/FeedProvider.test.ts +++ b/src/providers/FeedProvider.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest' +import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr' import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' -import { buildAllFavoritesFeedRelayUrls } from '@/lib/home-feed-relays' +import { buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays' describe('home feed relay policy', () => { it('keeps aggr.nostr.land out of the main home feed', () => { @@ -16,4 +17,25 @@ describe('home feed relay policy', () => { expect(urls).not.toContain('wss://aggr.nostr.land/') expect(urls).not.toContain(AGGR_NOSTR_LAND_WSS) }) + + it('home reply merge omits aggr even when listed on viewer read relays', () => { + const merged = stripNostrLandAggrFromRelayUrls( + feedRelayPolicyUrls( + [ + { source: 'favorites', urls: ['wss://relay.example/'] }, + { source: 'viewer-read', urls: [AGGR_NOSTR_LAND_WSS, 'wss://inbox.example/'] } + ], + { + operation: 'read', + blockedRelays: [], + nostrLandAggr: 'never', + applySocialKindBlockedFilter: false, + allowThirdPartyLocalRelays: true + } + ) + ) + expect(merged).not.toContain(AGGR_NOSTR_LAND_WSS) + expect(merged).toContain('wss://relay.example/') + expect(merged).toContain('wss://inbox.example/') + }) }) diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx index 5ed00b12..b0b1bc91 100644 --- a/src/providers/FeedProvider.tsx +++ b/src/providers/FeedProvider.tsx @@ -1,7 +1,7 @@ import { FAST_READ_RELAY_URLS } from '@/constants' import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata' -import { buildAllFavoritesFeedRelayUrls } from '@/lib/home-feed-relays' +import { buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays' import logger from '@/lib/logger' import { syncViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility' import { normalizeAnyRelayUrl } from '@/lib/url' @@ -29,19 +29,23 @@ function buildHomeReplyFeedRelayUrls( httpRelayUrls: string[], blockedRelays: string[] ): string[] { - return feedRelayPolicyUrls( - [ - { source: 'favorites', urls: primaryRelayUrls }, - { source: 'viewer-read', urls: inboxRelayUrls }, - { source: 'cache', urls: cacheRelayUrls }, - { source: 'http-index', urls: httpRelayUrls } - ], - { - operation: 'read', - blockedRelays, - applySocialKindBlockedFilter: false, - allowThirdPartyLocalRelays: true - } + /** Home Replies/Gallery: never prepend aggr (reserved for side-panel threads, profiles, spells). */ + return stripNostrLandAggrFromRelayUrls( + feedRelayPolicyUrls( + [ + { source: 'favorites', urls: primaryRelayUrls }, + { source: 'viewer-read', urls: inboxRelayUrls }, + { source: 'cache', urls: cacheRelayUrls }, + { source: 'http-index', urls: httpRelayUrls } + ], + { + operation: 'read', + blockedRelays, + nostrLandAggr: 'never', + applySocialKindBlockedFilter: false, + allowThirdPartyLocalRelays: true + } + ) ) } @@ -60,18 +64,18 @@ export function FeedProvider({ children }: { children: ReactNode }) { */ const primaryExtraRelayUrls = useMemo(() => [buildWispTrendingNotesRelayUrl()], []) - /** Home Replies widen to relays that can surface inbox/reply context. */ + /** Read-side layers merged into {@link replyRelayUrls}; {@link outboxRelayUrls} is only for aggr eligibility sync. */ const replyExtraRelayLayers = useMemo(() => { const cacheRelayUrls: string[] = [] if (cacheRelayListEvent) { const list = getRelayListFromEvent(cacheRelayListEvent, blockedRelays) - cacheRelayUrls.push(...list.read, ...list.write) + cacheRelayUrls.push(...list.read) } - const httpRelayUrls: string[] = [...(relayList?.httpRead ?? []), ...(relayList?.httpWrite ?? [])] + const httpRelayUrls: string[] = [...(relayList?.httpRead ?? [])] if (httpRelayListEvent) { const list = getHttpRelayListFromEvent(httpRelayListEvent, blockedRelays) - httpRelayUrls.push(...list.httpRead, ...list.httpWrite) + httpRelayUrls.push(...list.httpRead) } return { @@ -106,7 +110,8 @@ export function FeedProvider({ children }: { children: ReactNode }) { [] ) - const viewerNostrLandAggrEligible = useMemo(() => { + /** Keeps {@link getViewerRelayStackNostrLandAggrEligible} in sync for non-home reads (threads, profiles, etc.). */ + useEffect(() => { const urls = [ ...favoriteFeedRelayUrls, ...replyExtraRelayLayers.inboxRelayUrls, @@ -114,7 +119,7 @@ export function FeedProvider({ children }: { children: ReactNode }) { ...replyExtraRelayLayers.cacheRelayUrls, ...replyExtraRelayLayers.httpRelayUrls ] - return syncViewerRelayStackNostrLandAggrEligible(urls) + syncViewerRelayStackNostrLandAggrEligible(urls) }, [favoriteFeedRelayUrls, replyExtraRelayLayers]) const lastHomeFeedUrlLogRef = useRef({ primary: '', reply: '' }) @@ -139,14 +144,7 @@ export function FeedProvider({ children }: { children: ReactNode }) { } setUrlStateIfChanged(setRelayUrls, primaryRelays) setUrlStateIfChanged(setReplyRelayUrls, replyRelays) - }, [ - favoriteFeedRelayUrls, - blockedRelays, - primaryExtraRelayUrls, - replyExtraRelayLayers, - setUrlStateIfChanged, - viewerNostrLandAggrEligible - ]) + }, [favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls, replyExtraRelayLayers, setUrlStateIfChanged]) const favoriteRelaysIdentity = useMemo( () => @@ -170,7 +168,6 @@ export function FeedProvider({ children }: { children: ReactNode }) { () => [ ...replyExtraRelayLayers.inboxRelayUrls, - ...replyExtraRelayLayers.outboxRelayUrls, ...replyExtraRelayLayers.cacheRelayUrls, ...replyExtraRelayLayers.httpRelayUrls ] @@ -191,7 +188,6 @@ export function FeedProvider({ children }: { children: ReactNode }) { relaySets.length, favoriteFeedRelayUrls.length - favoriteRelays.length, replyExtraRelayLayers.inboxRelayUrls.length, - replyExtraRelayLayers.outboxRelayUrls.length, replyExtraRelayLayers.cacheRelayUrls.length, replyExtraRelayLayers.httpRelayUrls.length, blockedRelays.length @@ -206,7 +202,6 @@ export function FeedProvider({ children }: { children: ReactNode }) { relaySets: relaySets.length, relaySetRelays: favoriteFeedRelayUrls.length - favoriteRelays.length, inboxRelays: replyExtraRelayLayers.inboxRelayUrls.length, - outboxRelays: replyExtraRelayLayers.outboxRelayUrls.length, cacheRelays: replyExtraRelayLayers.cacheRelayUrls.length, httpRelays: replyExtraRelayLayers.httpRelayUrls.length, blockedRelays: blockedRelays.length diff --git a/src/providers/feed-context.tsx b/src/providers/feed-context.tsx index 914051a4..04a371c9 100644 --- a/src/providers/feed-context.tsx +++ b/src/providers/feed-context.tsx @@ -5,9 +5,12 @@ import { createContext, useContext } from 'react' export type TFeedContext = { - /** Home Notes/Gallery: favorites plus mixed trending discovery. */ + /** Home Notes (OP): favorites plus Wisp trending only — no aggr, no FAST_READ padding. */ relayUrls: string[] - /** Home Replies: primary feed relays plus viewer inbox, HTTP, cache, and eligible aggregator relays. */ + /** + * Home Replies + Gallery: same OP base, then NIP-65 read inboxes (FAST_READ when empty), kind 10432 cache + * read relays, HTTP read index — never aggr (aggr is for side-panel threads, profiles, and spells only). + */ replyRelayUrls: string[] }