|
|
|
@ -1,10 +1,13 @@ |
|
|
|
|
|
|
|
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' |
|
|
|
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' |
|
|
|
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' |
|
|
|
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' |
|
|
|
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' |
|
|
|
import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata' |
|
|
|
import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata' |
|
|
|
import logger from '@/lib/logger' |
|
|
|
import logger from '@/lib/logger' |
|
|
|
|
|
|
|
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr' |
|
|
|
import { normalizeAnyRelayUrl } from '@/lib/url' |
|
|
|
import { normalizeAnyRelayUrl } from '@/lib/url' |
|
|
|
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' |
|
|
|
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' |
|
|
|
import { useEffect, useMemo, useState, useCallback } from 'react' |
|
|
|
import { useEffect, useMemo, useState, useCallback } from 'react' |
|
|
|
|
|
|
|
import type { Dispatch, ReactNode, SetStateAction } from 'react' |
|
|
|
import { FeedContext } from './feed-context' |
|
|
|
import { FeedContext } from './feed-context' |
|
|
|
import { useFavoriteRelays } from './FavoriteRelaysProvider' |
|
|
|
import { useFavoriteRelays } from './FavoriteRelaysProvider' |
|
|
|
import { useNostr } from './NostrProvider' |
|
|
|
import { useNostr } from './NostrProvider' |
|
|
|
@ -37,54 +40,133 @@ function buildAllFavoritesFeedRelayUrls( |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function FeedProvider({ children }: { children: React.ReactNode }) { |
|
|
|
function relayListMentionsNostrLand(urls: readonly string[]): boolean { |
|
|
|
const { isInitialized, cacheRelayListEvent, httpRelayListEvent } = useNostr() |
|
|
|
return urls.some((url) => { |
|
|
|
const { favoriteRelays, blockedRelays } = useFavoriteRelays() |
|
|
|
const normalized = normalizeAnyRelayUrl(url) || url.trim() |
|
|
|
|
|
|
|
if (!normalized) return false |
|
|
|
/** |
|
|
|
try { |
|
|
|
* Extra relay URLs always merged into the all-favorites feed: |
|
|
|
const parsed = new URL(normalized.replace(/^ws:\/\//i, 'http://').replace(/^wss:\/\//i, 'https://')) |
|
|
|
* - Cache relays (kind 10432) if the user has configured any |
|
|
|
return parsed.hostname.toLowerCase() === 'nostr.land' |
|
|
|
* - HTTP index relays (kind 10243) if the user has configured any |
|
|
|
} catch { |
|
|
|
* - The Wisp trending relay (always included) |
|
|
|
return false |
|
|
|
*/ |
|
|
|
} |
|
|
|
const extraFeedRelayUrls = useMemo(() => { |
|
|
|
}) |
|
|
|
const extra: string[] = [buildWispTrendingNotesRelayUrl()] |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildHomeReplyFeedRelayUrls( |
|
|
|
|
|
|
|
primaryRelayUrls: string[], |
|
|
|
|
|
|
|
inboxRelayUrls: string[], |
|
|
|
|
|
|
|
cacheRelayUrls: string[], |
|
|
|
|
|
|
|
httpRelayUrls: string[], |
|
|
|
|
|
|
|
includeNostrLandAggr: boolean, |
|
|
|
|
|
|
|
blockedRelays: string[] |
|
|
|
|
|
|
|
): string[] { |
|
|
|
|
|
|
|
return feedRelayPolicyUrls([ |
|
|
|
|
|
|
|
{ source: 'favorites', urls: primaryRelayUrls }, |
|
|
|
|
|
|
|
{ source: 'viewer-read', urls: inboxRelayUrls }, |
|
|
|
|
|
|
|
{ source: 'cache', urls: cacheRelayUrls }, |
|
|
|
|
|
|
|
{ source: 'http-index', urls: httpRelayUrls }, |
|
|
|
|
|
|
|
...(includeNostrLandAggr ? [{ source: 'read-only', urls: [AGGR_NOSTR_LAND_WSS] }] : []) |
|
|
|
|
|
|
|
], { |
|
|
|
|
|
|
|
operation: 'read', |
|
|
|
|
|
|
|
blockedRelays, |
|
|
|
|
|
|
|
nostrLandAggr: 'never', |
|
|
|
|
|
|
|
applySocialKindBlockedFilter: false, |
|
|
|
|
|
|
|
allowThirdPartyLocalRelays: true |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function FeedProvider({ children }: { children: ReactNode }) { |
|
|
|
|
|
|
|
const { isInitialized, relayList, cacheRelayListEvent, httpRelayListEvent } = useNostr() |
|
|
|
|
|
|
|
const { favoriteRelays, blockedRelays, relaySets } = useFavoriteRelays() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const favoriteFeedRelayUrls = useMemo( |
|
|
|
|
|
|
|
() => [...favoriteRelays, ...relaySets.flatMap((relaySet) => relaySet.relayUrls)], |
|
|
|
|
|
|
|
[favoriteRelays, relaySets] |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Home Notes/Gallery stay focused: favorites/defaults plus the mixed trending relay. */ |
|
|
|
|
|
|
|
const primaryExtraRelayUrls = useMemo(() => [buildWispTrendingNotesRelayUrl()], []) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Home Replies widen to relays that can surface inbox/reply context. */ |
|
|
|
|
|
|
|
const replyExtraRelayLayers = useMemo(() => { |
|
|
|
|
|
|
|
const cacheRelayUrls: string[] = [] |
|
|
|
if (cacheRelayListEvent) { |
|
|
|
if (cacheRelayListEvent) { |
|
|
|
const list = getRelayListFromEvent(cacheRelayListEvent) |
|
|
|
const list = getRelayListFromEvent(cacheRelayListEvent, blockedRelays) |
|
|
|
extra.push(...list.read, ...list.write) |
|
|
|
cacheRelayUrls.push(...list.read, ...list.write) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const httpRelayUrls: string[] = [...(relayList?.httpRead ?? []), ...(relayList?.httpWrite ?? [])] |
|
|
|
if (httpRelayListEvent) { |
|
|
|
if (httpRelayListEvent) { |
|
|
|
const list = getHttpRelayListFromEvent(httpRelayListEvent) |
|
|
|
const list = getHttpRelayListFromEvent(httpRelayListEvent, blockedRelays) |
|
|
|
extra.push(...list.httpRead, ...list.httpWrite) |
|
|
|
httpRelayUrls.push(...list.httpRead, ...list.httpWrite) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
inboxRelayUrls: relayList?.read?.length ? relayList.read : FAST_READ_RELAY_URLS, |
|
|
|
|
|
|
|
outboxRelayUrls: relayList?.write?.length ? relayList.write : FAST_WRITE_RELAY_URLS, |
|
|
|
|
|
|
|
cacheRelayUrls, |
|
|
|
|
|
|
|
httpRelayUrls |
|
|
|
} |
|
|
|
} |
|
|
|
return extra |
|
|
|
}, [relayList, cacheRelayListEvent, httpRelayListEvent, blockedRelays]) |
|
|
|
}, [cacheRelayListEvent, httpRelayListEvent]) |
|
|
|
|
|
|
|
/** Default relays immediately so feeds / sidebar REQ never wait on Nostr session restore. */ |
|
|
|
/** Default relays immediately so feeds / sidebar REQ never wait on Nostr session restore. */ |
|
|
|
const [relayUrls, setRelayUrls] = useState<string[]>(() => |
|
|
|
const [relayUrls, setRelayUrls] = useState<string[]>(() => |
|
|
|
buildAllFavoritesFeedRelayUrls([], [], [buildWispTrendingNotesRelayUrl()]) |
|
|
|
buildAllFavoritesFeedRelayUrls([], [], [buildWispTrendingNotesRelayUrl()]) |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
const [replyRelayUrls, setReplyRelayUrls] = useState<string[]>(() => |
|
|
|
|
|
|
|
buildHomeReplyFeedRelayUrls( |
|
|
|
|
|
|
|
buildAllFavoritesFeedRelayUrls([], [], [buildWispTrendingNotesRelayUrl()]), |
|
|
|
|
|
|
|
[], |
|
|
|
|
|
|
|
[], |
|
|
|
|
|
|
|
[], |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
) |
|
|
|
/** Same logical relay policy result — reuse array ref so NoteList does not re-subscribe. */ |
|
|
|
/** Same logical relay policy result — reuse array ref so NoteList does not re-subscribe. */ |
|
|
|
const setRelayUrlsIfChanged = useCallback((next: string[]) => { |
|
|
|
const setUrlStateIfChanged = useCallback( |
|
|
|
setRelayUrls((prev) => { |
|
|
|
(setter: Dispatch<SetStateAction<string[]>>, next: string[]) => { |
|
|
|
if (relayUrlListIdentity(prev) === relayUrlListIdentity(next)) return prev |
|
|
|
setter((prev) => { |
|
|
|
return next |
|
|
|
if (relayUrlListIdentity(prev) === relayUrlListIdentity(next)) return prev |
|
|
|
}) |
|
|
|
return next |
|
|
|
}, []) |
|
|
|
}) |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
const updateFeedRelayUrls = useCallback(() => { |
|
|
|
const updateFeedRelayUrls = useCallback(() => { |
|
|
|
const finalRelays = buildAllFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, extraFeedRelayUrls) |
|
|
|
const primaryRelays = buildAllFavoritesFeedRelayUrls(favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls) |
|
|
|
logger.debug('Updating all-favorites relay URLs:', finalRelays) |
|
|
|
const aggrEligibleRelayUrls = [ |
|
|
|
setRelayUrlsIfChanged(finalRelays) |
|
|
|
...favoriteFeedRelayUrls, |
|
|
|
}, [favoriteRelays, blockedRelays, extraFeedRelayUrls, setRelayUrlsIfChanged]) |
|
|
|
...replyExtraRelayLayers.inboxRelayUrls, |
|
|
|
|
|
|
|
...replyExtraRelayLayers.outboxRelayUrls, |
|
|
|
|
|
|
|
...replyExtraRelayLayers.cacheRelayUrls |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
const replyRelays = buildHomeReplyFeedRelayUrls( |
|
|
|
|
|
|
|
primaryRelays, |
|
|
|
|
|
|
|
replyExtraRelayLayers.inboxRelayUrls, |
|
|
|
|
|
|
|
replyExtraRelayLayers.cacheRelayUrls, |
|
|
|
|
|
|
|
replyExtraRelayLayers.httpRelayUrls, |
|
|
|
|
|
|
|
relayListMentionsNostrLand(aggrEligibleRelayUrls), |
|
|
|
|
|
|
|
blockedRelays |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
logger.debug('Updating home feed relay URLs:', { |
|
|
|
|
|
|
|
primaryRelays, |
|
|
|
|
|
|
|
replyRelays |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
setUrlStateIfChanged(setRelayUrls, primaryRelays) |
|
|
|
|
|
|
|
setUrlStateIfChanged(setReplyRelayUrls, replyRelays) |
|
|
|
|
|
|
|
}, [favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls, replyExtraRelayLayers, setUrlStateIfChanged]) |
|
|
|
|
|
|
|
|
|
|
|
const favoriteRelaysIdentity = useMemo( |
|
|
|
const favoriteRelaysIdentity = useMemo( |
|
|
|
() => |
|
|
|
() => |
|
|
|
[...favoriteRelays] |
|
|
|
[...favoriteFeedRelayUrls] |
|
|
|
.map((u) => normalizeAnyRelayUrl(u) || u.trim()) |
|
|
|
.map((u) => normalizeAnyRelayUrl(u) || u.trim()) |
|
|
|
.filter(Boolean) |
|
|
|
.filter(Boolean) |
|
|
|
.sort() |
|
|
|
.sort() |
|
|
|
.join('|'), |
|
|
|
.join('|'), |
|
|
|
[favoriteRelays] |
|
|
|
[favoriteFeedRelayUrls] |
|
|
|
) |
|
|
|
) |
|
|
|
const blockedRelaysIdentity = useMemo( |
|
|
|
const blockedRelaysIdentity = useMemo( |
|
|
|
() => |
|
|
|
() => |
|
|
|
@ -95,24 +177,45 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { |
|
|
|
.join('|'), |
|
|
|
.join('|'), |
|
|
|
[blockedRelays] |
|
|
|
[blockedRelays] |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
const replyExtraRelaysIdentity = useMemo( |
|
|
|
|
|
|
|
() => |
|
|
|
|
|
|
|
[ |
|
|
|
|
|
|
|
...replyExtraRelayLayers.inboxRelayUrls, |
|
|
|
|
|
|
|
...replyExtraRelayLayers.outboxRelayUrls, |
|
|
|
|
|
|
|
...replyExtraRelayLayers.cacheRelayUrls, |
|
|
|
|
|
|
|
...replyExtraRelayLayers.httpRelayUrls |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
.map((u) => normalizeAnyRelayUrl(u) || u.trim()) |
|
|
|
|
|
|
|
.filter(Boolean) |
|
|
|
|
|
|
|
.sort() |
|
|
|
|
|
|
|
.join('|'), |
|
|
|
|
|
|
|
[replyExtraRelayLayers] |
|
|
|
|
|
|
|
) |
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
logger.debug('FeedProvider relay init:', { |
|
|
|
logger.debug('FeedProvider relay init:', { |
|
|
|
isInitialized, |
|
|
|
isInitialized, |
|
|
|
favoriteRelays: favoriteRelays.length, |
|
|
|
favoriteRelays: favoriteRelays.length, |
|
|
|
|
|
|
|
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 |
|
|
|
blockedRelays: blockedRelays.length |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
if (favoriteRelays.length === 0) { |
|
|
|
if (favoriteFeedRelayUrls.length === 0) { |
|
|
|
logger.debug('FeedProvider: favoriteRelays is empty, using defaults') |
|
|
|
logger.debug('FeedProvider: no favorite or relay-set relays, using defaults') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
updateFeedRelayUrls() |
|
|
|
updateFeedRelayUrls() |
|
|
|
}, [isInitialized, favoriteRelaysIdentity, blockedRelaysIdentity, updateFeedRelayUrls]) |
|
|
|
}, [isInitialized, favoriteRelaysIdentity, blockedRelaysIdentity, replyExtraRelaysIdentity, updateFeedRelayUrls]) |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<FeedContext.Provider |
|
|
|
<FeedContext.Provider |
|
|
|
value={{ |
|
|
|
value={{ |
|
|
|
relayUrls |
|
|
|
relayUrls, |
|
|
|
|
|
|
|
replyRelayUrls |
|
|
|
}} |
|
|
|
}} |
|
|
|
> |
|
|
|
> |
|
|
|
{children} |
|
|
|
{children} |
|
|
|
|