Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
d9e4b40a1b
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 7
      src/components/Explore/ExploreFavoriteRelays.tsx
  4. 16
      src/components/NormalFeed/index.tsx
  5. 20
      src/components/NoteList/index.tsx
  6. 2
      src/components/ReplyNoteList/index.tsx
  7. 14
      src/hooks/use-global-relay-bootstrap-defaults.ts
  8. 12
      src/hooks/useProfileAuthorFeedSubRequests.ts
  9. 4
      src/hooks/useProfilePins.tsx
  10. 10
      src/hooks/useProfileTimeline.tsx
  11. 14
      src/lib/account-list-relay-urls.ts
  12. 76
      src/lib/event-metadata.ts
  13. 52
      src/lib/favorites-feed-relays.ts
  14. 102
      src/lib/home-feed-relays.ts
  15. 49
      src/lib/live-activities.ts
  16. 66
      src/lib/relay-list-builder.ts
  17. 19
      src/lib/relay-url-priority.ts
  18. 54
      src/lib/viewer-relay-defaults.ts
  19. 41
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  20. 7
      src/pages/primary/SpellsPage/CreateSpellDialog.tsx
  21. 8
      src/pages/primary/SpellsPage/index.tsx
  22. 17
      src/pages/secondary/NoteListPage/index.tsx
  23. 149
      src/providers/FavoriteRelaysActivityProvider.tsx
  24. 27
      src/providers/FavoriteRelaysProvider.tsx
  25. 47
      src/providers/FeedProvider.test.ts
  26. 42
      src/providers/FeedProvider.tsx
  27. 17
      src/providers/LiveActivitiesProvider.tsx
  28. 40
      src/providers/NostrProvider/index.tsx
  29. 68
      src/services/client.service.ts
  30. 7
      src/services/spell.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
{
"name": "imwald",
"version": "23.10.0",
"version": "23.9.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "imwald",
"version": "23.10.0",
"version": "23.9.2",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "imwald",
"version": "23.10.0",
"version": "23.9.2",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true,
"type": "module",

7
src/components/Explore/ExploreFavoriteRelays.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '@/components/RelaySimpleInfo'
import { Button } from '@/components/ui/button'
import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { useFetchRelayInfo } from '@/hooks'
import { toRelay, toRelaySettings } from '@/lib/link'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
@ -61,6 +62,7 @@ export default function ExploreFavoriteRelays() { @@ -61,6 +62,7 @@ export default function ExploreFavoriteRelays() {
const { navigate } = usePrimaryPage()
const { push } = useSecondaryPage()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const blockedSet = useMemo(
() => new Set(blockedRelays.map((b) => normalizeUrl(b) || b)),
@ -75,6 +77,9 @@ export default function ExploreFavoriteRelays() { @@ -75,6 +77,9 @@ export default function ExploreFavoriteRelays() {
if (visible.length > 0) {
return { urls: visible, usingDefaults: false }
}
if (!useGlobalRelayBootstrap) {
return { urls: [], usingDefaults: false }
}
const defaultsFiltered = DEFAULT_FAVORITE_RELAYS.filter((r) => {
const k = normalizeUrl(r) || r
return k && !blockedSet.has(k)
@ -83,7 +88,7 @@ export default function ExploreFavoriteRelays() { @@ -83,7 +88,7 @@ export default function ExploreFavoriteRelays() {
urls: defaultsFiltered.length > 0 ? defaultsFiltered : DEFAULT_FAVORITE_RELAYS,
usingDefaults: true
}
}, [favoriteRelays, blockedSet])
}, [favoriteRelays, blockedSet, useGlobalRelayBootstrap])
if (urls.length === 0) return null

16
src/components/NormalFeed/index.tsx

@ -2,6 +2,7 @@ import storage from '@/services/local-storage.service' @@ -2,6 +2,7 @@ import storage from '@/services/local-storage.service'
import NoteList, { TNoteListRef } from '@/components/NoteList'
import { RefreshButton } from '@/components/RefreshButton'
import Tabs, { TabDefinition } from '@/components/Tabs'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import { PROFILE_MEDIA_TAB_KINDS, FAST_READ_RELAY_URLS } from '@/constants'
@ -27,7 +28,10 @@ import KindFilter from '../KindFilter' @@ -27,7 +28,10 @@ import KindFilter from '../KindFilter'
* Home Gallery: favorites (or chip relays) first, then {@link FAST_READ_RELAY_URLS} so NIP-71 / picture / voice
* events are not starved when the users relay set is mostly text timelines. Deduped by normalized URL.
*/
function galleryRelayUrlsMergedWithReadLayer(favoriteUrls: readonly string[]): string[] {
function galleryRelayUrlsMergedWithReadLayer(
favoriteUrls: readonly string[],
mergeGlobalFastRead: boolean
): string[] {
const seen = new Set<string>()
const out: string[] = []
const add = (raw: string) => {
@ -39,7 +43,9 @@ function galleryRelayUrlsMergedWithReadLayer(favoriteUrls: readonly string[]): s @@ -39,7 +43,9 @@ function galleryRelayUrlsMergedWithReadLayer(favoriteUrls: readonly string[]): s
out.push(n)
}
for (const u of favoriteUrls) add(u)
for (const u of FAST_READ_RELAY_URLS) add(u)
if (mergeGlobalFastRead) {
for (const u of FAST_READ_RELAY_URLS) add(u)
}
return out
}
@ -155,6 +161,7 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -155,6 +161,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
ref
) {
const { hideUntrustedNotes } = useUserTrust()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const { showKinds, showKind1OPs, showKind1Replies, showKind1111, feedKindFilterBypass } =
useKindFilterOrDefaults()
const [listMode, setListMode] = useState<TNoteListMode>(() => {
@ -219,7 +226,7 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -219,7 +226,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
isMainFeed && mainFeedGalleryRelayUrls && mainFeedGalleryRelayUrls.length > 0
? mainFeedGalleryRelayUrls
: isMainFeed && widenMainGalleryRelays
? galleryRelayUrlsMergedWithReadLayer(req.urls)
? galleryRelayUrlsMergedWithReadLayer(req.urls, useGlobalRelayBootstrap)
: req.urls,
filter: { ...req.filter, kinds: MEDIA_KINDS }
}))
@ -230,7 +237,8 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -230,7 +237,8 @@ const NormalFeed = forwardRef<TNoteListRef, {
MEDIA_KINDS,
isMainFeed,
widenMainGalleryRelays,
mainFeedGalleryRelayUrls
mainFeedGalleryRelayUrls,
useGlobalRelayBootstrap
])
const noteListExtraShouldHide = useMemo(() => {

20
src/components/NoteList/index.tsx

@ -100,6 +100,7 @@ import { @@ -100,6 +100,7 @@ import {
stableFeedKindKey
} from '@/features/feed/descriptor'
import { mapNoteListSubRequestsForTimeline } from '@/features/feed/note-list-requests'
import { stripNostrLandAggrFromTimelineSubRequests } from '@/lib/home-feed-relays'
import { createFetchEventsFeedRuntimeLoader } from '@/features/feed/client-loader'
import { FeedRuntime } from '@/features/feed/runtime'
import { buildFeedDiagnosticsSnapshot, logFeedDiagnostics } from '@/features/feed/diagnostics'
@ -1893,7 +1894,10 @@ const NoteList = forwardRef( @@ -1893,7 +1894,10 @@ const NoteList = forwardRef(
let diskPrimeCancelled = false
const primeDiskWhileAwaitingRelayProbe = async () => {
try {
const mapped = mapLiveSubRequestsForTimeline(subRequestsRef.current)
const mapped = stripNostrLandAggrFromTimelineSubRequests(
feedSubscriptionKey,
mapLiveSubRequestsForTimeline(subRequestsRef.current)
)
.map((req) =>
isOfflineRef.current
? { ...req, urls: req.urls.filter((u) => isLocalNetworkUrl(u)) }
@ -1975,7 +1979,10 @@ const NoteList = forwardRef( @@ -1975,7 +1979,10 @@ const NoteList = forwardRef(
const seeAllNoSpell = seeAllFeedEventsRef.current && !useFilterAsIsRef.current
const mappedSubRequests = mapLiveSubRequestsForTimeline(subRequestsRef.current)
const mappedSubRequests = stripNostrLandAggrFromTimelineSubRequests(
feedSubscriptionKey,
mapLiveSubRequestsForTimeline(subRequestsRef.current)
)
.map((req) =>
isOfflineRef.current
? { ...req, urls: req.urls.filter((u) => isLocalNetworkUrl(u)) }
@ -3064,6 +3071,7 @@ const NoteList = forwardRef( @@ -3064,6 +3071,7 @@ const NoteList = forwardRef(
}
}, [
timelineSubscriptionKey,
feedSubscriptionKey,
sessionSnapshotIdentityKey,
subRequestsKey,
preserveTimelineOnSubRequestsChange,
@ -3104,7 +3112,10 @@ const NoteList = forwardRef( @@ -3104,7 +3112,10 @@ const NoteList = forwardRef(
if (!tk) return
let deltaActive = true
const mappedDelta = mapLiveSubRequestsForTimeline(deltas)
const mappedDelta = stripNostrLandAggrFromTimelineSubRequests(
feedSubscriptionKey,
mapLiveSubRequestsForTimeline(deltas)
)
const seeAllNoSpellDelta = seeAllFeedEventsRef.current && !useFilterAsIsRef.current
const filterMissingKindsDelta = (f: Filter) => !f.kinds || f.kinds.length === 0
const invalidDelta = mappedDelta.filter(({ urls, filter: f }) => {
@ -3335,6 +3346,7 @@ const NoteList = forwardRef( @@ -3335,6 +3346,7 @@ const NoteList = forwardRef(
followingFeedDeltaSubRequestsKey,
timelineKey,
oneShotFetch,
feedSubscriptionKey,
mapLiveSubRequestsForTimeline,
areAlgoRelays,
allowKindlessRelayExplore,
@ -3511,6 +3523,7 @@ const NoteList = forwardRef( @@ -3511,6 +3523,7 @@ const NoteList = forwardRef(
useEffect(() => {
if (!timelinePublicReadFallback) return
if (feedSubscriptionKey === 'home-all-favorites') return
if (oneShotFetch || areAlgoRelays) return
if (!navigator.onLine) return
if (feedFullSearchEvents !== null) return
@ -3587,6 +3600,7 @@ const NoteList = forwardRef( @@ -3587,6 +3600,7 @@ const NoteList = forwardRef(
})()
}, [
timelinePublicReadFallback,
feedSubscriptionKey,
oneShotFetch,
areAlgoRelays,
progressiveWarmupQuery,

2
src/components/ReplyNoteList/index.tsx

@ -1084,7 +1084,7 @@ function ReplyNoteList({ @@ -1084,7 +1084,7 @@ function ReplyNoteList({
if (!rootInfo) return // Type guard
try {
// READ from: FAST_READ_RELAY_URLS + user's inboxes + local relays + OP author's outboxes
// READ from: thread hints, author/user NIP-65, favorites, cache — then DEFAULT_FAVORITE_RELAYS fallback.
const opAuthorPubkey = rootInfo.type === 'E' || rootInfo.type === 'A' ? rootInfo.pubkey : undefined
const seenOn = client.getSeenEventRelayUrls(event.id).map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)
const fromBrowsingFeed = browsingRelayUrls.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)

14
src/hooks/use-global-relay-bootstrap-defaults.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
/** @returns true when the app may merge FAST_READ / FAST_WRITE / DEFAULT_FAVORITE bootstrap relays. */
export function useGlobalRelayBootstrapDefaults(): boolean {
const { pubkey, relayList } = useNostr()
const { favoriteRelays } = useFavoriteRelays()
return viewerUsesGlobalRelayDefaults({
viewerPubkey: pubkey,
favoriteRelayUrls: favoriteRelays,
relayList
})
}

12
src/hooks/useProfileAuthorFeedSubRequests.ts

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
import { buildProfileAuthorSubRequestsFromUrlGroups } from '@/lib/profile-author-subrequests'
import { isSocialKindBlockedKind } from '@/constants'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeAnyRelayUrl } from '@/lib/url'
@ -6,7 +8,6 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -6,7 +8,6 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostrOptional } from '@/providers/nostr-context'
import client from '@/services/client.service'
import type { TFeedSubRequest } from '@/types'
import { isSocialKindBlockedKind } from '@/constants'
import { useCallback, useEffect, useMemo, useState } from 'react'
function relayListsContentKey(favoriteRelays: string[], blockedRelays: string[]): string {
@ -41,6 +42,7 @@ export function useProfileAuthorFeedSubRequests({ @@ -41,6 +42,7 @@ export function useProfileAuthorFeedSubRequests({
} {
const nostr = useNostrOptional()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const includeAuthorLocalRelays = useMemo(() => {
const me = nostr?.pubkey?.trim()
@ -80,7 +82,8 @@ export function useProfileAuthorFeedSubRequests({ @@ -80,7 +82,8 @@ export function useProfileAuthorFeedSubRequests({
emptyAuthor,
socialKinds,
includeAuthorLocalRelays,
kinds
kinds,
useGlobalRelayBootstrap
)
if (!cancelled) {
setProvisionalUrls(provisional)
@ -98,7 +101,8 @@ export function useProfileAuthorFeedSubRequests({ @@ -98,7 +101,8 @@ export function useProfileAuthorFeedSubRequests({
authorRl,
socialKinds,
includeAuthorLocalRelays,
kinds
kinds,
useGlobalRelayBootstrap
)
setFullUrls(full)
})
@ -109,7 +113,7 @@ export function useProfileAuthorFeedSubRequests({ @@ -109,7 +113,7 @@ export function useProfileAuthorFeedSubRequests({
// `relayListsKey` already fingerprints `favoriteRelays` + `blockedRelays` by sorted URL content.
// Do not list those arrays here: the provider often hands new `[]` references each render and would
// retrigger this effect forever (setState → re-render → new refs → effect → …).
}, [pubkey, relayListsKey, kindsKey, kinds, refreshToken, includeAuthorLocalRelays])
}, [pubkey, relayListsKey, kindsKey, kinds, refreshToken, includeAuthorLocalRelays, useGlobalRelayBootstrap])
const activeUrls = fullUrls?.length ? fullUrls : provisionalUrls

4
src/hooks/useProfilePins.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { Event } from 'nostr-tools'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import {
buildAuthorInboxOutboxRelayUrls,
buildProfileAugmentedReadRelayUrls,
@ -79,6 +80,7 @@ function blockedRelaysContentKey(blockedRelays: string[]): string { @@ -79,6 +80,7 @@ function blockedRelaysContentKey(blockedRelays: string[]): string {
export function useProfilePins(pubkey: string | undefined) {
const nostr = useNostrOptional()
const { blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const blockedKey = useMemo(() => blockedRelaysContentKey(blockedRelays), [blockedRelays])
const includeAuthorLocalRelays = useMemo(() => {
const me = nostr?.pubkey?.trim()
@ -179,7 +181,7 @@ export function useProfilePins(pubkey: string | undefined) { @@ -179,7 +181,7 @@ export function useProfilePins(pubkey: string | undefined) {
client.fetchPinListEvent(pk).catch(() => undefined)
])
const authorRelays = buildAuthorInboxOutboxRelayUrls(authorRl, blockedRelays, includeAuthorLocalRelays)
const pinsResolveRelays = buildProfileAugmentedReadRelayUrls(authorRelays, blockedRelays)
const pinsResolveRelays = buildProfileAugmentedReadRelayUrls(authorRelays, blockedRelays, 16, useGlobalRelayBootstrap)
if (!pinsResolveRelays.length) {
if (!paintedLocalPins) setPinEvents([])
return

10
src/hooks/useProfileTimeline.tsx

@ -3,6 +3,7 @@ import client, { eventService } from '@/services/client.service' @@ -3,6 +3,7 @@ import client, { eventService } from '@/services/client.service'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event, kinds as nostrKinds, type Filter } from 'nostr-tools'
import { CALENDAR_EVENT_KINDS, ExtendedKind, isDocumentRelayKind, isSocialKindBlockedKind } from '@/constants'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeAnyRelayUrl, subtractNormalizedRelayUrls } from '@/lib/url'
@ -130,6 +131,7 @@ export function useProfileTimeline({ @@ -130,6 +131,7 @@ export function useProfileTimeline({
}: UseProfileTimelineOptions): UseProfileTimelineResult {
const nostr = useNostrOptional()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const includeAuthorLocalRelays = useMemo(() => {
const me = nostr?.pubkey?.trim()
if (!me) return false
@ -311,7 +313,8 @@ export function useProfileTimeline({ @@ -311,7 +313,8 @@ export function useProfileTimeline({
emptyAuthor,
socialKinds,
includeAuthorLocalRelays,
kinds
kinds,
useGlobalRelayBootstrap
)
const startWave = async (subRequests: ReturnType<typeof buildSubRequests>) => {
@ -406,7 +409,8 @@ export function useProfileTimeline({ @@ -406,7 +409,8 @@ export function useProfileTimeline({
authorRl,
socialKinds,
includeAuthorLocalRelays,
kinds
kinds,
useGlobalRelayBootstrap
)
const deltaUrls = subtractNormalizedRelayUrls(fullFeedUrls, provisionalFeedUrls)
if (cancelled || deltaUrls.length === 0) return
@ -439,7 +443,7 @@ export function useProfileTimeline({ @@ -439,7 +443,7 @@ export function useProfileTimeline({
subscriptionRef.current()
subscriptionRef.current = () => {}
}
}, [pubkey, cacheKey, JSON.stringify(kinds), limit, refreshToken, relayListsKey, includeAuthorLocalRelays])
}, [pubkey, cacheKey, JSON.stringify(kinds), limit, refreshToken, relayListsKey, includeAuthorLocalRelays, useGlobalRelayBootstrap])
const refresh = useCallback(() => {
subscriptionRef.current()

14
src/lib/account-list-relay-urls.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays'
import { buildPrioritizedReadRelayUrls, buildPrioritizedWriteRelayUrls } from '@/lib/relay-url-priority'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import client from '@/services/client.service'
/**
@ -14,21 +15,28 @@ export async function buildAccountListRelayUrlsForMerge(options: { @@ -14,21 +15,28 @@ export async function buildAccountListRelayUrlsForMerge(options: {
}): Promise<string[]> {
const { accountPubkey, favoriteRelays, blockedRelays } = options
const myRelayList = await client.fetchRelayList(accountPubkey)
const favoritesTier = getFavoritesFeedRelayUrls(favoriteRelays ?? [], blockedRelays)
const useGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: accountPubkey,
favoriteRelayUrls: favoriteRelays ?? [],
relayList: myRelayList
})
const favoritesTier = getFavoritesFeedRelayUrls(favoriteRelays ?? [], blockedRelays, useGlobal)
const read = buildPrioritizedReadRelayUrls({
userReadRelays: myRelayList.read ?? [],
userWriteRelays: myRelayList.write ?? [],
favoriteRelays: favoritesTier,
blockedRelays,
maxRelays: 100,
applySocialKindBlockedFilter: false
applySocialKindBlockedFilter: false,
includeGlobalFastRead: useGlobal
})
const write = buildPrioritizedWriteRelayUrls({
userWriteRelays: myRelayList.write ?? [],
favoriteRelays: favoritesTier,
blockedRelays,
maxRelays: 100,
applySocialKindBlockedFilter: false
applySocialKindBlockedFilter: false,
includeGlobalFastWriteReadTails: useGlobal
})
const merged = [...read, ...write]
return [...new Set(merged.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean))]

76
src/lib/event-metadata.ts

@ -16,8 +16,30 @@ const emptyHttpRelayListFields = { @@ -16,8 +16,30 @@ const emptyHttpRelayListFields = {
httpOriginalRelays: [] as TMailboxRelay[]
}
export function getRelayListFromEvent(event?: Event | null, blockedRelays?: string[]) {
export type GetRelayListFromEventOptions = {
/**
* When false, never substitute {@link FAST_READ_RELAY_URLS} / {@link FAST_WRITE_RELAY_URLS} for missing or
* oversized lists (use `[]` or the first 8 entries instead). Default true for anonymous / bootstrap callers.
*/
globalReadWriteFallback?: boolean
}
export function getRelayListFromEvent(
event?: Event | null,
blockedRelays?: string[],
options?: GetRelayListFromEventOptions
) {
const globalFb = options?.globalReadWriteFallback !== false
if (!event) {
if (!globalFb) {
return {
write: [] as string[],
read: [] as string[],
originalRelays: [] as TRelayList['originalRelays'],
...emptyHttpRelayListFields
}
}
return {
write: FAST_WRITE_RELAY_URLS,
read: FAST_READ_RELAY_URLS,
@ -61,14 +83,62 @@ export function getRelayListFromEvent(event?: Event | null, blockedRelays?: stri @@ -61,14 +83,62 @@ export function getRelayListFromEvent(event?: Event | null, blockedRelays?: stri
// If there are too many relays, use the default inbox/outbox relays.
// Because they don't know anything about relays, their settings cannot be trusted
const readOut =
relayList.read.length && relayList.read.length <= 8
? relayList.read
: globalFb
? FAST_READ_RELAY_URLS
: relayList.read.slice(0, 8)
const writeOut =
relayList.write.length && relayList.write.length <= 8
? relayList.write
: globalFb
? FAST_WRITE_RELAY_URLS
: relayList.write.slice(0, 8)
return {
write: relayList.write.length && relayList.write.length <= 8 ? relayList.write : FAST_WRITE_RELAY_URLS,
read: relayList.read.length && relayList.read.length <= 8 ? relayList.read : FAST_READ_RELAY_URLS,
write: writeOut,
read: readOut,
originalRelays: relayList.originalRelays,
...emptyHttpRelayListFields
}
}
/**
* Read-side `r` tags from a relay list event (e.g. kind 10012) without {@link FAST_READ_RELAY_URLS} fallback
* when the list is empty or oversized for strict viewer-owned REQ stacks (relay pulse).
*/
export function getRelayListReadFromEventNoFastFallback(
event: Event | null | undefined,
blockedRelays?: string[]
): string[] {
if (!event) return []
const torBrowserDetected = isTorBrowser()
const normalizedBlockedRelays = (blockedRelays || []).map((url) => normalizeUrl(url) || url)
const read: string[] = []
event.tags.filter(tagNameEquals('r')).forEach(([, url, type]) => {
if (!url || typeof url !== 'string' || url.trim() === '' || url === 'ws://' || url === 'wss://') return
if (!isWebsocketUrl(url)) return
const normalizedUrl = normalizeUrl(url)
if (!normalizedUrl) return
if (normalizedBlockedRelays.includes(normalizedUrl)) return
if (normalizedUrl.endsWith('.onion/') && !torBrowserDetected) return
if (type === 'write') return
if (type === 'read') {
read.push(normalizedUrl)
} else {
read.push(normalizedUrl)
}
})
if (read.length === 0) return []
if (read.length <= 8) return read
return read.slice(0, 8)
}
/** Kind 10243: `r` tags with http(s) URLs only; same read/write/both semantics as NIP-65. */
export function getHttpRelayListFromEvent(event?: Event | null, blockedRelays?: string[]) {
const out = {

52
src/lib/favorites-feed-relays.ts

@ -18,6 +18,7 @@ import { @@ -18,6 +18,7 @@ import {
} from '@/lib/relay-url-priority'
import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy'
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
import { profileFetchRelayUrlsWithoutFastReadLayer } from '@/lib/viewer-relay-defaults'
const blockedSet = (blockedRelays: string[]) =>
new Set(blockedRelays.map((b) => normalizeAnyRelayUrl(b) || b))
@ -25,8 +26,8 @@ const blockedSet = (blockedRelays: string[]) => @@ -25,8 +26,8 @@ const blockedSet = (blockedRelays: string[]) =>
/**
* Logged-in users favorite relays (kind 10012 `relay` tags via {@link useFavoriteRelays}, plus bootstrap defaults
* when the event is missing): drop blocked, dedupe, normalize. If no non-blocked entries remain, use
* {@link DEFAULT_FAVORITE_RELAYS}. Same list drives the favorites tier in REQ/publish prioritization and the
* all-favorites home feed.
* {@link DEFAULT_FAVORITE_RELAYS} only when `useGlobalFavoriteDefaults` is true (signed-out or no NIP-65 and no favorites).
* Same list drives the favorites tier in REQ/publish prioritization and the all-favorites home feed.
*/
/**
* NIP-65 `read` plus HTTP index inboxes (kind 10243) for feed REQ / query URL lists.
@ -41,14 +42,15 @@ export function userReadRelaysWithHttp( @@ -41,14 +42,15 @@ export function userReadRelaysWithHttp(
export function getFavoritesFeedRelayUrls(
favoriteRelays: string[],
blockedRelays: string[]
blockedRelays: string[],
useGlobalFavoriteDefaults = true
): string[] {
const blocked = blockedSet(blockedRelays)
const visible = favoriteRelays.filter((r) => {
const k = normalizeAnyRelayUrl(r) || r
return k && !blocked.has(k)
})
const base = visible.length > 0 ? visible : DEFAULT_FAVORITE_RELAYS
const base = visible.length > 0 ? visible : useGlobalFavoriteDefaults ? DEFAULT_FAVORITE_RELAYS : []
return feedRelayPolicyUrls(
[{ source: 'favorites', urls: base }],
{
@ -107,10 +109,14 @@ const PROFILE_AUGMENTED_READ_MAX_RELAYS = 16 @@ -107,10 +109,14 @@ const PROFILE_AUGMENTED_READ_MAX_RELAYS = 16
export function buildProfileAugmentedReadRelayUrls(
authorRelayUrls: string[],
blockedRelays: string[],
maxRelays: number = PROFILE_AUGMENTED_READ_MAX_RELAYS
maxRelays: number = PROFILE_AUGMENTED_READ_MAX_RELAYS,
useGlobalRelayBootstrap = true
): string[] {
const readOnlyLayer = READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean)
const fastReadLayer = FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean)
const fastReadLayer =
useGlobalRelayBootstrap
? (FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[])
: []
const merged = mergeRelayUrlLayers([authorRelayUrls, readOnlyLayer, fastReadLayer], blockedRelays)
return merged.slice(0, maxRelays)
}
@ -127,6 +133,12 @@ export type ReadRelayPriorityOptions = { @@ -127,6 +133,12 @@ export type ReadRelayPriorityOptions = {
* relays in `SOCIAL_KIND_BLOCKED_RELAY_URLS` before capping.
*/
applySocialKindBlockedFilter?: boolean
/**
* When false, empty favorites do not fall back to {@link DEFAULT_FAVORITE_RELAYS}. Default true.
*/
useGlobalFavoriteDefaults?: boolean
/** When false, omit the global FAST_READ tier. Default true. */
includeGlobalFastRead?: boolean
}
/**
@ -138,7 +150,9 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox( @@ -138,7 +150,9 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox(
userInboxReadRelays: string[],
options?: ReadRelayPriorityOptions
): string[] {
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays)
const useFavDefaults = options?.useGlobalFavoriteDefaults !== false
const includeFast = options?.includeGlobalFastRead !== false
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, useFavDefaults)
return buildPrioritizedReadRelayUrls({
userReadRelays: userInboxReadRelays,
userWriteRelays: options?.userWriteRelays ?? [],
@ -146,7 +160,8 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox( @@ -146,7 +160,8 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays: favorites,
blockedRelays,
maxRelays: options?.maxRelays,
applySocialKindBlockedFilter: options?.applySocialKindBlockedFilter
applySocialKindBlockedFilter: options?.applySocialKindBlockedFilter,
includeGlobalFastRead: includeFast
})
}
@ -169,8 +184,11 @@ export function buildProfilePageReadRelayUrls( @@ -169,8 +184,11 @@ export function buildProfilePageReadRelayUrls(
kindsIncludeSocialBlockedKind: boolean,
includeAuthorLocalRelays = false,
/** When the timeline includes document kinds (30023, 30040, …), add document index relays and raise the cap. */
profileKindsHint?: readonly number[]
profileKindsHint?: readonly number[],
/** When false, omit global FAST_READ / profile-fetch widening for logged-in users with their own relay stack. */
useGlobalRelayBootstrap?: boolean
): string[] {
const useGlobal = useGlobalRelayBootstrap !== false
const wantsDocumentLayer = profileKindsHint?.some((k) => isDocumentRelayKind(k)) ?? false
const maxRelays = wantsDocumentLayer ? PROFILE_PAGE_DOCUMENT_FEED_MAX_RELAYS : PROFILE_PAGE_FEED_MAX_RELAYS
const list = includeAuthorLocalRelays
@ -180,8 +198,10 @@ export function buildProfilePageReadRelayUrls( @@ -180,8 +198,10 @@ export function buildProfilePageReadRelayUrls(
const authorWrite = [...(list.httpWrite ?? []), ...(list.write ?? [])]
const authorHasNoNip65 = authorRead.length === 0 && authorWrite.length === 0
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays)
const fastReadLayer = FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, useGlobal)
const fastReadLayer = useGlobal
? (FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[])
: []
const authorWriteLayer = relayUrlsLocalsFirst(authorWrite)
const authorReadLayer = relayUrlsLocalsFirst(authorRead)
const urls = feedRelayPolicyUrls(
@ -202,7 +222,8 @@ export function buildProfilePageReadRelayUrls( @@ -202,7 +222,8 @@ export function buildProfilePageReadRelayUrls(
)
/** Authors without kind 10002: widen REQ targets so notes/metadata are still discoverable on index relays. */
if (authorHasNoNip65) {
const profileFetchLayer = PROFILE_FETCH_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
const profileSource = useGlobal ? PROFILE_FETCH_RELAY_URLS : profileFetchRelayUrlsWithoutFastReadLayer()
const profileFetchLayer = profileSource.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
return mergeRelayUrlLayers([urls, profileFetchLayer], blockedRelays).slice(0, maxRelays + 8)
}
if (wantsDocumentLayer) {
@ -235,7 +256,9 @@ export function augmentSubRequestsWithFavoritesFastReadAndInbox( @@ -235,7 +256,9 @@ export function augmentSubRequestsWithFavoritesFastReadAndInbox(
? options.applySocialKindBlockedFilter
: relayFilterIncludesSocialKindBlockedKind(r.filter)
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays)
const useFavDefaults = options?.useGlobalFavoriteDefaults !== false
const includeFast = options?.includeGlobalFastRead !== false
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, useFavDefaults)
const authorOnly = dedupeNormalizeRelayUrlsOrdered(options?.authorWriteRelays ?? [])
@ -243,7 +266,8 @@ export function augmentSubRequestsWithFavoritesFastReadAndInbox( @@ -243,7 +266,8 @@ export function augmentSubRequestsWithFavoritesFastReadAndInbox(
userReadRelays: userInboxReadRelays,
userWriteRelays: options?.userWriteRelays ?? [],
authorWriteRelays: authorOnly,
favoriteRelays: favorites
favoriteRelays: favorites,
includeGlobalFastRead: includeFast
})
const layers = [relayUrlsLocalsFirst(r.urls), ...coreLayers]

102
src/lib/home-feed-relays.ts

@ -1,12 +1,24 @@ @@ -1,12 +1,24 @@
import { MAX_REQ_RELAY_URLS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { getHttpRelayListFromEvent, getRelayListReadFromEventNoFastFallback } from '@/lib/event-metadata'
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays'
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import type { Event } from 'nostr-tools'
function relayUrlIsNostrLandAggr(url: string): boolean {
const normalized = (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
const aggr = (normalizeAnyRelayUrl(AGGR_NOSTR_LAND_WSS) || AGGR_NOSTR_LAND_WSS).toLowerCase()
return normalized === aggr
const raw = url.trim()
if (!raw) return false
const normalized = (normalizeAnyRelayUrl(raw) || raw).toLowerCase()
const aggrCanon = (normalizeAnyRelayUrl(AGGR_NOSTR_LAND_WSS) || AGGR_NOSTR_LAND_WSS).toLowerCase()
if (normalized === aggrCanon) return true
try {
const u = new URL(normalized)
return u.hostname.toLowerCase() === 'aggr.nostr.land'
} catch {
return /^wss:\/\/aggr\.nostr\.land\/?$/i.test(normalized)
}
}
/** Drop nostr.land aggregate from REQ stacks where it must not appear (e.g. home feeds). */
@ -14,15 +26,36 @@ export function stripNostrLandAggrFromRelayUrls(urls: readonly string[]): string @@ -14,15 +26,36 @@ export function stripNostrLandAggrFromRelayUrls(urls: readonly string[]): string
return urls.filter((url) => !relayUrlIsNostrLandAggr(url))
}
/**
* Home Lieblings-Relays feed must never open timeline REQs to nostr.lands aggregate relay (reserved for
* threads / profiles / spells). Strips aggr from every shard after mapping, including trailing-slash variants.
*/
export function stripNostrLandAggrFromTimelineSubRequests<T extends { urls: string[] }>(
feedSubscriptionKey: string | undefined,
requests: readonly T[]
): T[] {
if (feedSubscriptionKey !== 'home-all-favorites') {
return requests.slice() as T[]
}
return requests.map((r) => ({
...r,
urls: stripNostrLandAggrFromRelayUrls(r.urls)
})) as T[]
}
export function buildAllFavoritesFeedRelayUrls(
favoriteRelays: string[],
blockedRelays: string[],
extraFeedRelayUrls: string[]
extraFeedRelayUrls: string[],
useGlobalFavoriteDefaults = true
): string[] {
return stripNostrLandAggrFromRelayUrls(
feedRelayPolicyUrls(
[
{ source: 'favorites', urls: getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) },
{
source: 'favorites',
urls: getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, useGlobalFavoriteDefaults)
},
{ source: 'fallback', urls: extraFeedRelayUrls }
],
{
@ -35,3 +68,62 @@ export function buildAllFavoritesFeedRelayUrls( @@ -35,3 +68,62 @@ export function buildAllFavoritesFeedRelayUrls(
)
)
}
/**
* Relay pulse (sidebar active authors): only the viewers own stack favorites (+ relay sets),
* NIP-65 read, kind 10012 cache read, and HTTP index reads never the global fast-read layer.
*/
export function buildRelayPulseQueryRelayUrls(options: {
viewerPubkey: string | null | undefined
favoriteRelayUrls: string[]
blockedRelays: string[]
relayList: { read?: string[]; httpRead?: string[] } | null | undefined
cacheRelayListEvent: Event | null | undefined
httpRelayListEvent: Event | null | undefined
}): string[] {
const {
viewerPubkey,
favoriteRelayUrls,
blockedRelays,
relayList,
cacheRelayListEvent,
httpRelayListEvent
} = options
const useGlobalFavoriteDefaults = viewerUsesGlobalRelayDefaults({
viewerPubkey,
favoriteRelayUrls,
relayList
})
const primaryRelays = getFavoritesFeedRelayUrls(favoriteRelayUrls, blockedRelays, useGlobalFavoriteDefaults)
const inboxRelayUrls = relayList?.read?.length ? relayList.read : []
const cacheRelayUrls: string[] = []
if (cacheRelayListEvent) {
cacheRelayUrls.push(...getRelayListReadFromEventNoFastFallback(cacheRelayListEvent, blockedRelays))
}
const httpRelayUrls: string[] = [...(relayList?.httpRead ?? [])]
if (httpRelayListEvent) {
httpRelayUrls.push(...getHttpRelayListFromEvent(httpRelayListEvent, blockedRelays).httpRead)
}
return stripNostrLandAggrFromRelayUrls(
feedRelayPolicyUrls(
[
{ source: 'favorites', urls: primaryRelays },
{ source: 'viewer-read', urls: inboxRelayUrls },
{ source: 'cache', urls: cacheRelayUrls },
{ source: 'http-index', urls: httpRelayUrls }
],
{
operation: 'read',
blockedRelays,
nostrLandAggr: 'never',
applySocialKindBlockedFilter: false,
allowThirdPartyLocalRelays: true,
maxRelays: MAX_REQ_RELAY_URLS
}
)
)
}

49
src/lib/live-activities.ts

@ -662,21 +662,31 @@ export function buildLiveActivitiesRelayUrls(options: { @@ -662,21 +662,31 @@ export function buildLiveActivitiesRelayUrls(options: {
blockedRelays: string[]
relayListRead: string[]
relayListWrite: string[]
/**
* When false for a logged-in viewer with their own relay stack, omit {@link FAST_READ_RELAY_URLS} and skip
* {@link DEFAULT_FAVORITE_RELAYS} when favorites are empty. Default true (signed-out / bootstrap).
*/
includeGlobalFastRead?: boolean
}): string[] {
const { loggedIn, favoriteRelays, blockedRelays, relayListRead, relayListWrite } = options
const includeFast = options.includeGlobalFastRead !== false
const useGlobalFavoriteDefaults = includeFast
if (loggedIn) {
const fav = relayUrlsLocalsFirst(getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays))
const fav = relayUrlsLocalsFirst(
getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, useGlobalFavoriteDefaults)
)
const read = relayUrlsLocalsFirst(relayListRead)
const write = relayUrlsLocalsFirst(relayListWrite)
const fast = dedupeNormalizeRelayUrlsOrdered(
FAST_READ_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)
)
return feedRelayPolicyUrls([
{ source: 'favorites', urls: fav },
{ source: 'viewer-read', urls: read },
{ source: 'viewer-write', urls: write },
{ source: 'fast-read', urls: fast }
], {
const layers = [
{ source: 'favorites' as const, urls: fav },
{ source: 'viewer-read' as const, urls: read },
{ source: 'viewer-write' as const, urls: write },
...(includeFast ? [{ source: 'fast-read' as const, urls: fast }] : [])
]
return feedRelayPolicyUrls(layers, {
operation: 'read',
blockedRelays,
maxRelays: MAX_REQ_RELAY_URLS,
@ -684,20 +694,23 @@ export function buildLiveActivitiesRelayUrls(options: { @@ -684,20 +694,23 @@ export function buildLiveActivitiesRelayUrls(options: {
allowThirdPartyLocalRelays: true
})
}
const fav = relayUrlsLocalsFirst(getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays))
const fav = relayUrlsLocalsFirst(getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, true))
const fast = dedupeNormalizeRelayUrlsOrdered(
FAST_READ_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)
)
return feedRelayPolicyUrls([
{ source: 'favorites', urls: fav },
{ source: 'fast-read', urls: fast }
], {
operation: 'read',
blockedRelays,
maxRelays: MAX_REQ_RELAY_URLS,
applySocialKindBlockedFilter: true,
allowThirdPartyLocalRelays: true
})
return feedRelayPolicyUrls(
[
{ source: 'favorites', urls: fav },
{ source: 'fast-read', urls: fast }
],
{
operation: 'read',
blockedRelays,
maxRelays: MAX_REQ_RELAY_URLS,
applySocialKindBlockedFilter: true,
allowThirdPartyLocalRelays: true
}
)
}
/** Milliseconds until the next wall-clock quarter hour (:00, :15, :30, :45). */

66
src/lib/relay-list-builder.ts

@ -11,10 +11,11 @@ @@ -11,10 +11,11 @@
import { FAST_READ_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { mergeRelayUrlLayers, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import { isHttpRelayUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { getCacheRelayUrls } from './private-relays'
import { defaultFavoriteRelaysForViewer, viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import client from '@/services/client.service'
import logger from '@/lib/logger'
import type { Event } from 'nostr-tools'
@ -249,21 +250,38 @@ export async function buildExploreProfileAndUserRelayList( @@ -249,21 +250,38 @@ export async function buildExploreProfileAndUserRelayList(
if (!userPubkey) {
return boot
}
let useGlobal = true
try {
const [fav, peeked] = await Promise.all([
client.fetchFavoriteRelays(userPubkey).catch(() => [] as string[]),
client.peekRelayListFromStorage(userPubkey).catch(() => null)
])
useGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: userPubkey,
favoriteRelayUrls: fav,
relayList: peeked ?? undefined
})
} catch {
useGlobal = true
}
try {
const built = await buildComprehensiveRelayList({
userPubkey,
includeUserOwnRelays: true,
includeProfileFetchRelays: true,
includeFastReadRelays: true,
includeFastReadRelays: useGlobal,
includeFavoriteRelays: false,
includeLocalRelays: true,
includeFastWriteRelays: false,
includeSearchableRelays: false
})
if (!useGlobal) {
return built
}
if (!built.length) return boot
return dedupeNormalizedRelayUrls([...boot, ...built])
} catch {
return boot
return useGlobal ? boot : []
}
}
@ -330,6 +348,7 @@ export async function buildPollResultsReadRelayUrls(options: { @@ -330,6 +348,7 @@ export async function buildPollResultsReadRelayUrls(options: {
let authorReadSlice: string[] = []
let viewerReadSlice: string[] = []
let useGlobalFastRead = true
try {
const [authorRl, viewerRl] = await Promise.all([
pollEvent.pubkey ? client.peekRelayListFromStorage(pollEvent.pubkey) : Promise.resolve(null),
@ -340,6 +359,11 @@ export async function buildPollResultsReadRelayUrls(options: { @@ -340,6 +359,11 @@ export async function buildPollResultsReadRelayUrls(options: {
}
if (viewerRl) {
viewerReadSlice = userReadRelaysWithHttp(viewerRl).slice(0, POLL_RESULTS_NIP65_READ_SLICE)
useGlobalFastRead = viewerUsesGlobalRelayDefaults({
viewerPubkey,
favoriteRelayUrls: viewerFavoriteRelayUrls,
relayList: viewerRl
})
}
} catch {
/* ignore — poll results still use other layers */
@ -357,7 +381,9 @@ export async function buildPollResultsReadRelayUrls(options: { @@ -357,7 +381,9 @@ export async function buildPollResultsReadRelayUrls(options: {
}
}
pushLayer([...FAST_READ_RELAY_URLS])
if (useGlobalFastRead) {
pushLayer([...FAST_READ_RELAY_URLS])
}
pushLayer(authorReadSlice)
return feedRelayPolicyUrls([{ source: 'fallback', urls: ordered }], {
@ -370,8 +396,8 @@ export async function buildPollResultsReadRelayUrls(options: { @@ -370,8 +396,8 @@ export async function buildPollResultsReadRelayUrls(options: {
}
/**
* Build relay list for reading replies/comments
* READ from: FAST_READ_RELAY_URLS + user's inboxes/outboxes + local relays + OP author's outboxes
* Build relay list for reading replies/comments: thread hints, author/user NIP-65, favorites, cache
* then default favorite relays only when global bootstrap applies (signed-out or no configured stack).
*/
export async function buildReplyReadRelayList(
opAuthorPubkey: string | undefined,
@ -379,18 +405,34 @@ export async function buildReplyReadRelayList( @@ -379,18 +405,34 @@ export async function buildReplyReadRelayList(
blockedRelays: string[] = [],
threadRelayHints: string[] = []
): Promise<string[]> {
return buildComprehensiveRelayList({
let useGlobal = true
if (userPubkey) {
try {
const [fav, rl] = await Promise.all([
client.fetchFavoriteRelays(userPubkey).catch(() => [] as string[]),
client.peekRelayListFromStorage(userPubkey)
])
useGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: userPubkey,
favoriteRelayUrls: fav,
relayList: rl ?? undefined
})
} catch {
useGlobal = true
}
}
const scoped = await buildComprehensiveRelayList({
authorPubkey: opAuthorPubkey,
userPubkey,
relayHints: threadRelayHints,
includeUserOwnRelays: Boolean(userPubkey),
includeFastReadRelays: true,
includeSearchableRelays: true,
includeFastReadRelays: useGlobal,
includeSearchableRelays: false,
includeLocalRelays: true,
/** Same menu list as timelines — threads often opened from favorites. */
includeFavoriteRelays: Boolean(userPubkey),
/** FAST_READ + SEARCHABLE before author/user NIP-65 slices so broken personal relays do not starve thread REQ under the global connection cap. */
preferPublicReadRelaysEarly: true,
preferPublicReadRelaysEarly: false,
includeProfileFetchRelays: useGlobal,
blockedRelays
})
return mergeRelayUrlLayers([scoped, defaultFavoriteRelaysForViewer(useGlobal)], blockedRelays)
}

19
src/lib/relay-url-priority.ts

@ -71,6 +71,8 @@ export function buildReadRelayPriorityLayers(opts: { @@ -71,6 +71,8 @@ export function buildReadRelayPriorityLayers(opts: {
userWriteRelays?: string[]
authorWriteRelays?: string[]
favoriteRelays: string[]
/** When false, omit the global FAST_READ tier (logged-in users with their own relay stack). Default true. */
includeGlobalFastRead?: boolean
}): string[][] {
const userWrite = opts.userWriteRelays ?? []
const writeLocals = userWrite.filter((u) => {
@ -81,7 +83,7 @@ export function buildReadRelayPriorityLayers(opts: { @@ -81,7 +83,7 @@ export function buildReadRelayPriorityLayers(opts: {
const tier1 = dedupeNormalizeRelayUrlsOrdered([...writeLocals, ...userReadOrdered])
const tier2 = dedupeNormalizeRelayUrlsOrdered(opts.authorWriteRelays ?? [])
const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? [])
const tier4 = normFastRead()
const tier4 = opts.includeGlobalFastRead === false ? [] : normFastRead()
return [tier1, tier2, tier3, tier4]
}
@ -98,6 +100,8 @@ export function buildPrioritizedReadRelayUrls(opts: { @@ -98,6 +100,8 @@ export function buildPrioritizedReadRelayUrls(opts: {
maxRelays?: number
/** Default true: strip {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} for social-kind-heavy timelines. Set false for other queries. */
applySocialKindBlockedFilter?: boolean
/** Default true: append global FAST_READ tier. */
includeGlobalFastRead?: boolean
}): string[] {
const max = opts.maxRelays ?? MAX_REQ_RELAY_URLS
const applySocial = opts.applySocialKindBlockedFilter !== false
@ -110,7 +114,8 @@ export function buildPrioritizedReadRelayUrls(opts: { @@ -110,7 +114,8 @@ export function buildPrioritizedReadRelayUrls(opts: {
userReadRelays: opts.userReadRelays,
userWriteRelays: opts.userWriteRelays,
authorWriteRelays: opts.authorWriteRelays,
favoriteRelays: opts.favoriteRelays
favoriteRelays: opts.favoriteRelays,
includeGlobalFastRead: opts.includeGlobalFastRead
})
const policyLayers: FeedRelayLayer[] = [
{ source: 'viewer-read', urls: layers[0] ?? [] },
@ -136,11 +141,16 @@ function buildWriteRelayPriorityLayers(opts: { @@ -136,11 +141,16 @@ function buildWriteRelayPriorityLayers(opts: {
authorReadRelays?: string[]
favoriteRelays?: string[]
extraRelays?: string[]
/** When false, omit global FAST_WRITE and FAST_READ tails. Default true. */
includeGlobalFastWriteReadTails?: boolean
}): string[][] {
const tier1 = relayUrlsLocalsFirst(opts.userWriteRelays)
const tier2 = filterContextAuthorReadRelaysForPublish(opts.authorReadRelays ?? [])
const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? [])
const tier4 = dedupeNormalizeRelayUrlsOrdered(opts.extraRelays ?? [])
if (opts.includeGlobalFastWriteReadTails === false) {
return [tier1, tier2, tier3, tier4, [], []]
}
const tier5 = normFastWrite()
const tier6 = normFastRead()
return [tier1, tier2, tier3, tier4, tier5, tier6]
@ -158,13 +168,16 @@ export function buildPrioritizedWriteRelayUrls(opts: { @@ -158,13 +168,16 @@ export function buildPrioritizedWriteRelayUrls(opts: {
maxRelays?: number
/** When true, strip {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} before capping (social kinds). */
applySocialKindBlockedFilter?: boolean
/** Default true: append FAST_WRITE then FAST_READ tiers. */
includeGlobalFastWriteReadTails?: boolean
}): string[] {
const max = opts.maxRelays ?? MAX_PUBLISH_RELAYS
const layers = buildWriteRelayPriorityLayers({
userWriteRelays: opts.userWriteRelays,
authorReadRelays: opts.authorReadRelays,
favoriteRelays: opts.favoriteRelays,
extraRelays: opts.extraRelays
extraRelays: opts.extraRelays,
includeGlobalFastWriteReadTails: opts.includeGlobalFastWriteReadTails
})
return feedRelayPolicyUrls([
{ source: 'viewer-write', urls: layers[0] ?? [] },

54
src/lib/viewer-relay-defaults.ts

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
import {
DEFAULT_FAVORITE_RELAYS,
FAST_READ_RELAY_URLS,
PROFILE_FETCH_RELAY_URLS
} from '@/constants'
import { normalizeUrl } from '@/lib/url'
export type ViewerRelayListLike = {
read?: string[] | null
write?: string[] | null
httpRead?: string[] | null
} | null | undefined
/**
* Use {@link DEFAULT_FAVORITE_RELAYS}, {@link FAST_READ_RELAY_URLS}, and {@link FAST_WRITE_RELAY_URLS} only when
* the user is not signed in, or when they are signed in but have configured neither favorite relays nor a NIP-65
* (kind 10002 / HTTP index) relay list. Otherwise REQ/publish stacks should stay on their own relays.
*/
export function viewerUsesGlobalRelayDefaults(args: {
viewerPubkey: string | null | undefined
favoriteRelayUrls: readonly string[]
relayList: ViewerRelayListLike
}): boolean {
if (!args.viewerPubkey?.trim()) return true
const hasFavorites = args.favoriteRelayUrls.some((u) => typeof u === 'string' && u.trim().length > 0)
const rl = args.relayList
const hasNip65 =
(rl?.read?.length ?? 0) > 0 ||
(rl?.write?.length ?? 0) > 0 ||
(rl?.httpRead?.length ?? 0) > 0
return !(hasFavorites || hasNip65)
}
const fastReadKeySet = (): Set<string> => {
const s = new Set<string>()
for (const u of FAST_READ_RELAY_URLS) {
const n = (normalizeUrl(u) || u).toLowerCase()
if (n) s.add(n)
}
return s
}
/** PROFILE_FETCH stack with {@link FAST_READ_RELAY_URLS} entries removed (order preserved). */
export function profileFetchRelayUrlsWithoutFastReadLayer(): string[] {
const drop = fastReadKeySet()
return PROFILE_FETCH_RELAY_URLS.filter((u) => {
const n = (normalizeUrl(u) || u).toLowerCase()
return n && !drop.has(n)
})
}
export function defaultFavoriteRelaysForViewer(useGlobalDefaults: boolean): string[] {
return useGlobalDefaults ? [...DEFAULT_FAVORITE_RELAYS] : []
}

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

@ -1,21 +1,12 @@ @@ -1,21 +1,12 @@
import NormalFeed from '@/components/NormalFeed'
import type { TNoteListRef } from '@/components/NoteList'
import { isReplyNoteEvent } from '@/lib/event'
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
import { checkAlgoRelay } from '@/lib/relay'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { normalizeUrl } from '@/lib/url'
import { useFeed } from '@/providers/feed-context'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import client from '@/services/client.service'
import relayInfoService from '@/services/relay-info.service'
import { kinds, type Event } from 'nostr-tools'
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'
const AGGR_RELAY_KEY = (normalizeAnyRelayUrl(AGGR_NOSTR_LAND_WSS) || AGGR_NOSTR_LAND_WSS).toLowerCase()
function relaySeenKey(url: string): string {
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
}
import { kinds } from 'nostr-tools'
import React, { forwardRef, useEffect, useMemo, useState } from 'react'
const RelaysFeed = forwardRef<
TNoteListRef,
@ -106,28 +97,6 @@ const RelaysFeed = forwardRef< @@ -106,28 +97,6 @@ const RelaysFeed = forwardRef<
}
]
}, [canRenderFeed, replyRelayUrls, relayUrls, defaultKinds])
const hideAggrOnlyMainFeedEvent = useCallback(
(event: Event) => {
const seenRelays = client.getSeenEventRelayUrls(event.id).map(relaySeenKey)
if (!seenRelays.includes(AGGR_RELAY_KEY)) return false
const allowedRelays = new Set(relayUrls.map(relaySeenKey))
return !seenRelays.some((relay) => relay !== AGGR_RELAY_KEY && allowedRelays.has(relay))
},
[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]
)
if (!canRenderFeed) {
return null
@ -150,10 +119,6 @@ const RelaysFeed = forwardRef< @@ -150,10 +119,6 @@ const RelaysFeed = forwardRef<
feedTimelineScopeKey="all-favorites"
showFeedClientFilter
hostPrimaryPageName="feed"
extraShouldHideEvent={hideAggrOnlyMainFeedEvent}
extraShouldHideGalleryEvent={hideAggrOnlyReplyGalleryStackEvent}
extraShouldHideRepliesEvent={hideAggrOnlyNonReplyEvent}
timelinePublicReadFallback
/>
)
})

7
src/pages/primary/SpellsPage/CreateSpellDialog.tsx

@ -18,6 +18,7 @@ import { @@ -18,6 +18,7 @@ import {
dedupeAppendIds,
resolveSpellListATags
} from '@/lib/spell-list-import'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { useBookmarks } from '@/providers/bookmarks-context'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
@ -294,6 +295,7 @@ export default function CreateSpellDialog({ @@ -294,6 +295,7 @@ export default function CreateSpellDialog({
const { pubkey, publish, checkLogin, relayList } = useNostr()
const { addBookmark, removeBookmark } = useBookmarks()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const [form, setForm] = useState<TSpellDraftParams>(DEFAULT_PARAMS)
const [saving, setSaving] = useState(false)
const scrollBodyRef = useRef<HTMLDivElement>(null)
@ -325,7 +327,8 @@ export default function CreateSpellDialog({ @@ -325,7 +327,8 @@ export default function CreateSpellDialog({
setForm(draft)
setListImportNotices(notices)
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? []
userWriteRelays: relayList?.write ?? [],
useGlobalRelayBootstrap
})
if (pendingATags.length === 0) return
void resolveSpellListATags(pendingATags, urls).then(({ ids, notices: extra }) => {
@ -335,7 +338,7 @@ export default function CreateSpellDialog({ @@ -335,7 +338,7 @@ export default function CreateSpellDialog({
if (extra.length) setListImportNotices((n) => [...n, ...extra])
})
},
[favoriteRelays, blockedRelays, relayList]
[favoriteRelays, blockedRelays, relayList, useGlobalRelayBootstrap]
)
const handleLoadManualList = useCallback(async () => {

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

@ -17,6 +17,7 @@ import { @@ -17,6 +17,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import PrimaryPageLayout, { type TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout'
import { usePrimaryPage } from '@/contexts/primary-page-context'
import logger from '@/lib/logger'
@ -89,6 +90,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -89,6 +90,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const { hideUntrustedNotifications } = useUserTrust()
const { isSmallScreen } = useScreenSize()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const {
showKinds: kindFilterShowKinds,
showKind1OPs,
@ -369,7 +371,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -369,7 +371,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
}
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? []
userWriteRelays: relayList?.write ?? [],
useGlobalRelayBootstrap
})
const catalogAuthors = buildSpellCatalogAuthors(pubkey, contacts)
const authorAllowlist = new Set(catalogAuthors)
@ -484,7 +487,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -484,7 +487,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
relayMailboxStableKey,
loadSpells,
contactsSyncKey,
spellCatalogManualRefreshKey
spellCatalogManualRefreshKey,
useGlobalRelayBootstrap
])
useEffect(() => {

17
src/pages/secondary/NoteListPage/index.tsx

@ -14,6 +14,7 @@ import { @@ -14,6 +14,7 @@ import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { toProfileList } from '@/lib/link'
import {
@ -47,6 +48,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -47,6 +48,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
const { push } = useSecondaryPage()
const { relayList, pubkey } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const interestList = useInterestListOptional()
const isSubscribed = interestList?.isSubscribed ?? (() => false)
const subscribe = interestList?.subscribe ?? (async () => {})
@ -109,7 +111,9 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -109,7 +111,9 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
.filter((k) => !isNaN(k))
const readUrlOpts = {
userWriteRelays: relayList?.write ?? [],
applySocialKindBlockedFilter: kinds.length === 0 || kinds.some(isSocialKindBlockedKind)
applySocialKindBlockedFilter: kinds.length === 0 || kinds.some(isSocialKindBlockedKind),
useGlobalFavoriteDefaults: useGlobalRelayBootstrap,
includeGlobalFastRead: useGlobalRelayBootstrap
}
const hashtag = searchParams.get('t')
const searchFromUrl = searchParams.get('s')
@ -205,7 +209,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -205,7 +209,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
{ userWriteRelays: relayList?.write ?? [], useGlobalFavoriteDefaults: useGlobalRelayBootstrap, includeGlobalFastRead: useGlobalRelayBootstrap }
)
}
])
@ -237,7 +241,11 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -237,7 +241,11 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
{
userWriteRelays: relayList?.write ?? [],
useGlobalFavoriteDefaults: useGlobalRelayBootstrap,
includeGlobalFastRead: useGlobalRelayBootstrap
}
)
)
setControls(
@ -294,7 +302,8 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -294,7 +302,8 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
t,
isSubscribed,
subscribe,
client
client,
useGlobalRelayBootstrap
])
// Initialize on mount

149
src/providers/FavoriteRelaysActivityProvider.tsx

@ -1,8 +1,7 @@ @@ -1,8 +1,7 @@
import storage from '@/services/local-storage.service'
import logger from '@/lib/logger'
import { ExtendedKind, NIP71_VIDEO_KINDS } from '@/constants'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { buildLiveActivitiesRelayUrls } from '@/lib/live-activities'
import { buildRelayPulseQueryRelayUrls } from '@/lib/home-feed-relays'
import {
readRelayPulseActiveNpubsCache,
writeRelayPulseActiveNpubsCache
@ -25,9 +24,14 @@ import { @@ -25,9 +24,14 @@ import {
const ACTIVE_WINDOW_SEC = 3600
/** Recent slice (seconds): newest notes dominate global REQ limits; a shorter window improves author diversity. */
const PULSE_RECENT_TAIL_SEC = 1200
/** Per-REQ event cap; two time slices run in parallel and merge (see {@link fetchRelayPulseNoteEvents}). */
const PULSE_REQ_LIMIT_RECENT = 900
const PULSE_REQ_LIMIT_EARLIER = 1400
/**
* Per-REQ event caps for the sidebar relay pulse. Keep small: each event is Schnorr-verified on the WebSocket
* thread in nostr-tools; limits of 900+1400 caused main-thread timeouts in verifyEvent when relays returned large batches.
*/
const PULSE_REQ_LIMIT_RECENT = 120
const PULSE_REQ_LIMIT_EARLIER = 160
/** Hard cap after merging two slices — enough for pubkey diversity without megabytes of verification work. */
const PULSE_MERGED_EVENT_CAP = 400
const FETCH_RETRY_DELAY_MS = 2500
/** Wall-clock cadence while the tab is visible */
const POLL_INTERVAL_MS = 60 * 60 * 1000
@ -94,7 +98,9 @@ async function fetchRelayPulseNoteEvents( @@ -94,7 +98,9 @@ async function fetchRelayPulseNoteEvents(
for (const r of settled) {
if (r.status === 'fulfilled') merged.push(...r.value)
}
return mergeRelayPulseEventsById(merged)
const deduped = mergeRelayPulseEventsById(merged)
deduped.sort((a, b) => b.created_at - a.created_at || a.id.localeCompare(b.id))
return deduped.slice(0, PULSE_MERGED_EVENT_CAP)
}
function aggregatePubkeysByRecency(events: { pubkey: string; created_at: number }[]): string[] {
@ -139,8 +145,9 @@ function partitionByFollows(orderedPubkeys: string[], followings: string[]) { @@ -139,8 +145,9 @@ function partitionByFollows(orderedPubkeys: string[], followings: string[]) {
}
export function FavoriteRelaysActivityProvider({ children }: { children: React.ReactNode }) {
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { pubkey: viewerPubkey, followListEvent, relayList } = useNostr()
const { favoriteRelays, blockedRelays, relaySets } = useFavoriteRelays()
const { pubkey: viewerPubkey, followListEvent, relayList, cacheRelayListEvent, httpRelayListEvent } =
useNostr()
const followings = useMemo(
() => (followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []),
[followListEvent]
@ -160,85 +167,87 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R @@ -160,85 +167,87 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R
orderedPubkeysRef.current = orderedPubkeys
/** After restoring from disk, ignore the first empty network result (timeouts / slow relays), then behave normally. */
const skipFirstEmptyNetworkOverwriteRef = useRef(false)
const favoriteRelayUrlsForPulse = useMemo(
() => [...favoriteRelays, ...relaySets.flatMap((rs) => rs.relayUrls)],
[favoriteRelays, relaySets]
)
const pulseQueryUrls = useMemo(
() =>
buildLiveActivitiesRelayUrls({
loggedIn: !!viewerPubkey,
favoriteRelays,
buildRelayPulseQueryRelayUrls({
viewerPubkey,
favoriteRelayUrls: favoriteRelayUrlsForPulse,
blockedRelays,
relayListRead: userReadRelaysWithHttp(relayList),
relayListWrite: relayList?.write ?? []
relayList,
cacheRelayListEvent,
httpRelayListEvent
}),
[viewerPubkey, favoriteRelays, blockedRelays, relayList]
[
viewerPubkey,
favoriteRelayUrlsForPulse,
blockedRelays,
relayList,
cacheRelayListEvent,
httpRelayListEvent
]
)
const relayKey = useMemo(() => pulseQueryUrls.join('\n'), [pulseQueryUrls])
const fetchActive = useCallback(
async (useDefaultRelays = false) => {
const cacheViewer = viewerPubkey ?? storage.getCurrentAccount()?.pubkey ?? null
const urls = useDefaultRelays
? buildLiveActivitiesRelayUrls({
loggedIn: false,
favoriteRelays: [],
blockedRelays,
relayListRead: [],
relayListWrite: []
})
: pulseQueryUrls
if (urls.length === 0) {
setLoading(false)
setRelayActivityReady(true)
const now = Date.now()
setOrderedPubkeys([])
const fetchActive = useCallback(async () => {
const cacheViewer = viewerPubkey ?? storage.getCurrentAccount()?.pubkey ?? null
const urls = pulseQueryUrls
if (urls.length === 0) {
setLoading(false)
setRelayActivityReady(true)
const now = Date.now()
setOrderedPubkeys([])
lastCompletedFetchAtRef.current = now
setLastFetchedAtMs(now)
writeRelayPulseActiveNpubsCache({
relayKey,
viewerPubkey: cacheViewer,
orderedPubkeys: [],
lastFetchedAtMs: now
})
return
}
setLoading(true)
const anchorSec = Math.floor(Date.now() / 1000)
try {
const events = await fetchRelayPulseNoteEvents(urls, anchorSec)
const now = Date.now()
const nextPubkeys = aggregatePubkeysByRecency(events)
const prev = orderedPubkeysRef.current
if (
skipFirstEmptyNetworkOverwriteRef.current &&
nextPubkeys.length === 0 &&
prev.length > 0
) {
skipFirstEmptyNetworkOverwriteRef.current = false
logger.debug('[FavoriteRelaysActivity] kept relay pulse from cache; first fetch returned empty')
} else {
skipFirstEmptyNetworkOverwriteRef.current = false
setOrderedPubkeys(nextPubkeys)
lastCompletedFetchAtRef.current = now
setLastFetchedAtMs(now)
writeRelayPulseActiveNpubsCache({
relayKey,
viewerPubkey: cacheViewer,
orderedPubkeys: [],
orderedPubkeys: nextPubkeys,
lastFetchedAtMs: now
})
return
}
setLoading(true)
const anchorSec = Math.floor(Date.now() / 1000)
try {
const events = await fetchRelayPulseNoteEvents(urls, anchorSec)
const now = Date.now()
const nextPubkeys = aggregatePubkeysByRecency(events)
const prev = orderedPubkeysRef.current
if (
skipFirstEmptyNetworkOverwriteRef.current &&
nextPubkeys.length === 0 &&
prev.length > 0
) {
skipFirstEmptyNetworkOverwriteRef.current = false
logger.debug('[FavoriteRelaysActivity] kept relay pulse from cache; first fetch returned empty')
} else {
skipFirstEmptyNetworkOverwriteRef.current = false
setOrderedPubkeys(nextPubkeys)
lastCompletedFetchAtRef.current = now
setLastFetchedAtMs(now)
writeRelayPulseActiveNpubsCache({
relayKey,
viewerPubkey: cacheViewer,
orderedPubkeys: nextPubkeys,
lastFetchedAtMs: now
})
}
} catch (error) {
logger.debug('[FavoriteRelaysActivity] fetch failed', { error, useDefaultRelays })
if (!useDefaultRelays && favoriteRelays.length > 0) {
setTimeout(() => void fetchRef.current(true), FETCH_RETRY_DELAY_MS)
}
} finally {
setLoading(false)
setRelayActivityReady(true)
} catch (error) {
logger.debug('[FavoriteRelaysActivity] fetch failed', { error })
if (pulseQueryUrls.length > 0) {
setTimeout(() => void fetchRef.current(), FETCH_RETRY_DELAY_MS)
}
},
[favoriteRelays, blockedRelays, relayKey, viewerPubkey, pulseQueryUrls]
)
} finally {
setLoading(false)
setRelayActivityReady(true)
}
}, [relayKey, viewerPubkey, pulseQueryUrls])
const fetchRef = useRef(fetchActive)
fetchRef.current = fetchActive

27
src/providers/FavoriteRelaysProvider.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { FAST_READ_RELAY_URLS, DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import storage from '@/services/local-storage.service'
import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
import { getReplaceableEventIdentifier } from '@/lib/event'
@ -25,11 +26,9 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -25,11 +26,9 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
useEffect(() => {
if (!favoriteRelaysEvent) {
/** Curated app defaults for the home feed — same for anonymous and logged-in users until kind 10012 loads. */
const favoriteRelays: string[] = [...DEFAULT_FAVORITE_RELAYS]
let favoriteRelays: string[] = []
if (pubkey) {
// Only add stored relay sets if user is logged in
const storedRelaySets = storage.getRelaySets()
storedRelaySets.forEach(({ relayUrls }) => {
relayUrls.forEach((url) => {
@ -40,6 +39,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -40,6 +39,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
})
}
const useGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: pubkey,
favoriteRelayUrls: favoriteRelays,
relayList
})
if (favoriteRelays.length === 0 && useGlobal) {
favoriteRelays = [...DEFAULT_FAVORITE_RELAYS]
}
setFavoriteRelays(favoriteRelays)
setRelaySetEvents([])
return
@ -82,9 +90,16 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -82,9 +90,16 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
)
setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[])
const relaySetDiscoverGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: pubkey,
favoriteRelayUrls: relays,
relayList
})
const normalizedRelays = [
...(relayList?.write ?? []).map(url => normalizeAnyRelayUrl(url) || url),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
...(relayList?.write ?? []).map((url) => normalizeAnyRelayUrl(url) || url),
...(relaySetDiscoverGlobal
? FAST_READ_RELAY_URLS.map((url) => normalizeUrl(url) || url)
: [])
]
const newRelaySetEvents = await queryService.fetchEvents(
Array.from(new Set(normalizedRelays)).slice(0, 5),
@ -121,7 +136,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -121,7 +136,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
)
}
init()
}, [favoriteRelaysEvent, pubkey])
}, [favoriteRelaysEvent, pubkey, relayList])
useEffect(() => {
if (!blockedRelaysEvent) {

47
src/providers/FeedProvider.test.ts

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest'
import { FAST_READ_RELAY_URLS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
import { buildRelayPulseQueryRelayUrls, buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import { buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays'
import type { Event } from 'nostr-tools'
describe('home feed relay policy', () => {
it('keeps aggr.nostr.land out of the main home feed', () => {
@ -38,4 +40,47 @@ describe('home feed relay policy', () => { @@ -38,4 +40,47 @@ describe('home feed relay policy', () => {
expect(merged).toContain('wss://relay.example/')
expect(merged).toContain('wss://inbox.example/')
})
it('stripNostrLandAggrFromRelayUrls removes aggr with trailing slash and hostname variants', () => {
const stripped = stripNostrLandAggrFromRelayUrls([
'wss://relay.example/',
'wss://aggr.nostr.land/',
AGGR_NOSTR_LAND_WSS,
'wss://AGGR.nostr.land'
])
expect(stripped).toEqual(['wss://relay.example/'])
})
it('relay pulse stack excludes global fast-read and aggr', () => {
const nineReadTags: string[][] = Array.from({ length: 9 }, (_, i) => [
'r',
`wss://many-${i}.example/`,
'read'
])
const oversizedCacheList = {
kind: 10012,
tags: [...nineReadTags],
content: '',
created_at: 0,
pubkey: 'a'.repeat(64),
id: 'b'.repeat(64),
sig: 'c'.repeat(128)
} satisfies Event
const urls = buildRelayPulseQueryRelayUrls({
viewerPubkey: 'd'.repeat(64),
favoriteRelayUrls: ['wss://fav.example/'],
blockedRelays: [],
relayList: { read: ['wss://nip65.example/'], httpRead: ['https://http-index.example/'] },
cacheRelayListEvent: oversizedCacheList,
httpRelayListEvent: null
})
for (const u of FAST_READ_RELAY_URLS) {
expect(urls).not.toContain(u)
}
expect(urls).not.toContain(AGGR_NOSTR_LAND_WSS)
expect(urls).not.toContain('wss://aggr.nostr.land/')
expect(urls.filter((u) => u.startsWith('wss://many-')).length).toBe(8)
})
})

42
src/providers/FeedProvider.tsx

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
import { FAST_READ_RELAY_URLS } from '@/constants'
import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata'
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'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
import type { Dispatch, ReactNode, SetStateAction } from 'react'
@ -50,9 +51,19 @@ function buildHomeReplyFeedRelayUrls( @@ -50,9 +51,19 @@ function buildHomeReplyFeedRelayUrls(
}
export function FeedProvider({ children }: { children: ReactNode }) {
const { isInitialized, relayList, cacheRelayListEvent, httpRelayListEvent } = useNostr()
const { isInitialized, relayList, cacheRelayListEvent, httpRelayListEvent, pubkey } = useNostr()
const { favoriteRelays, blockedRelays, relaySets } = useFavoriteRelays()
const useGlobalRelayDefaults = useMemo(
() =>
viewerUsesGlobalRelayDefaults({
viewerPubkey: pubkey,
favoriteRelayUrls: [...favoriteRelays, ...relaySets.flatMap((relaySet) => relaySet.relayUrls)],
relayList
}),
[pubkey, favoriteRelays, relaySets, relayList]
)
const favoriteFeedRelayUrls = useMemo(
() => [...favoriteRelays, ...relaySets.flatMap((relaySet) => relaySet.relayUrls)],
[favoriteRelays, relaySets]
@ -68,7 +79,9 @@ export function FeedProvider({ children }: { children: ReactNode }) { @@ -68,7 +79,9 @@ export function FeedProvider({ children }: { children: ReactNode }) {
const replyExtraRelayLayers = useMemo(() => {
const cacheRelayUrls: string[] = []
if (cacheRelayListEvent) {
const list = getRelayListFromEvent(cacheRelayListEvent, blockedRelays)
const list = getRelayListFromEvent(cacheRelayListEvent, blockedRelays, {
globalReadWriteFallback: useGlobalRelayDefaults
})
cacheRelayUrls.push(...list.read)
}
@ -79,12 +92,20 @@ export function FeedProvider({ children }: { children: ReactNode }) { @@ -79,12 +92,20 @@ export function FeedProvider({ children }: { children: ReactNode }) {
}
return {
inboxRelayUrls: relayList?.read?.length ? relayList.read : FAST_READ_RELAY_URLS,
outboxRelayUrls: relayList?.write?.length ? relayList.write : FAST_READ_RELAY_URLS,
inboxRelayUrls: relayList?.read?.length
? relayList.read
: useGlobalRelayDefaults
? DEFAULT_FAVORITE_RELAYS
: [],
outboxRelayUrls: relayList?.write?.length
? relayList.write
: useGlobalRelayDefaults
? DEFAULT_FAVORITE_RELAYS
: [],
cacheRelayUrls,
httpRelayUrls
}
}, [relayList, cacheRelayListEvent, httpRelayListEvent, blockedRelays])
}, [relayList, cacheRelayListEvent, httpRelayListEvent, blockedRelays, useGlobalRelayDefaults])
/** Default relays immediately so feeds / sidebar REQ never wait on Nostr session restore. */
const [relayUrls, setRelayUrls] = useState<string[]>(() =>
@ -124,7 +145,12 @@ export function FeedProvider({ children }: { children: ReactNode }) { @@ -124,7 +145,12 @@ export function FeedProvider({ children }: { children: ReactNode }) {
const lastHomeFeedUrlLogRef = useRef({ primary: '', reply: '' })
const updateFeedRelayUrls = useCallback(() => {
const primaryRelays = buildAllFavoritesFeedRelayUrls(favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls)
const primaryRelays = buildAllFavoritesFeedRelayUrls(
favoriteFeedRelayUrls,
blockedRelays,
primaryExtraRelayUrls,
useGlobalRelayDefaults
)
const replyRelays = buildHomeReplyFeedRelayUrls(
primaryRelays,
replyExtraRelayLayers.inboxRelayUrls,
@ -144,7 +170,7 @@ export function FeedProvider({ children }: { children: ReactNode }) { @@ -144,7 +170,7 @@ export function FeedProvider({ children }: { children: ReactNode }) {
}
setUrlStateIfChanged(setRelayUrls, primaryRelays)
setUrlStateIfChanged(setReplyRelayUrls, replyRelays)
}, [favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls, replyExtraRelayLayers, setUrlStateIfChanged])
}, [favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls, replyExtraRelayLayers, setUrlStateIfChanged, useGlobalRelayDefaults])
const favoriteRelaysIdentity = useMemo(
() =>

17
src/providers/LiveActivitiesProvider.tsx

@ -10,6 +10,7 @@ import { @@ -10,6 +10,7 @@ import {
} from '@/lib/live-activities'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { registerSessionInteractivePrewarmListener } from '@/services/session-interactive-prewarm-bridge'
@ -29,6 +30,16 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode @@ -29,6 +30,16 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode
const showLiveActivitiesBanner =
userPrefs?.showLiveActivitiesBanner ?? storage.getShowLiveActivitiesBanner()
const useGlobalBootstrap = useMemo(
() =>
viewerUsesGlobalRelayDefaults({
viewerPubkey: pubkey,
favoriteRelayUrls: favoriteRelays,
relayList
}),
[pubkey, favoriteRelays, relayList]
)
const [items, setItems] = useState<TLiveActivityItem[]>([])
const [loading, setLoading] = useState(false)
const [carouselHiddenAddresses, setCarouselHiddenAddresses] = useState<ReadonlySet<string>>(() => new Set())
@ -50,7 +61,8 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode @@ -50,7 +61,8 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode
favoriteRelays,
blockedRelays,
relayListRead: relayRead,
relayListWrite: relayWrite
relayListWrite: relayWrite,
includeGlobalFastRead: useGlobalBootstrap
})
if (urls.length === 0) {
rawItemsRef.current = []
@ -91,7 +103,8 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode @@ -91,7 +103,8 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode
blockedRelays,
relayRead,
relayWrite,
followings
followings,
useGlobalBootstrap
])
const toggleLiveActivityCarouselHidden = useCallback(async (address: string) => {

40
src/providers/NostrProvider/index.tsx

@ -25,6 +25,7 @@ import { getLatestEvent, minePow } from '@/lib/event' @@ -25,6 +25,7 @@ import { getLatestEvent, minePow } from '@/lib/event'
import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter'
import { getHttpRelayListFromEvent, getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import { LoginRequiredError } from '@/lib/nostr-errors'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
@ -66,18 +67,33 @@ export { useNostr } from '@/providers/nostr-context' @@ -66,18 +67,33 @@ export { useNostr } from '@/providers/nostr-context'
export type { TNostrContext } from '@/providers/nostr-context'
/** Kind 10012 `relay` tags for publish / target-relay prioritization. */
function favoriteRelayUrlsForPublish(favoriteRelaysEvent: Event | null, pubkey: string | null): string[] {
if (!favoriteRelaysEvent) {
return pubkey ? [...DEFAULT_FAVORITE_RELAYS] : []
function favoriteRelayUrlsForPublish(
favoriteRelaysEvent: Event | null,
pubkey: string | null,
relayList: TRelayList | null | undefined
): string[] {
const urlsFromEvent = (): string[] => {
const urls: string[] = []
if (!favoriteRelaysEvent) return urls
favoriteRelaysEvent.tags.forEach(([name, v]) => {
if (name === 'relay' && v) {
const n = normalizeAnyRelayUrl(v) || v
if (n && !urls.includes(n)) urls.push(n)
}
})
return urls
}
const urls: string[] = []
favoriteRelaysEvent.tags.forEach(([name, v]) => {
if (name === 'relay' && v) {
const n = normalizeAnyRelayUrl(v) || v
if (n && !urls.includes(n)) urls.push(n)
}
const fromEvent = urlsFromEvent()
const useGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: pubkey,
favoriteRelayUrls: fromEvent,
relayList
})
return urls.length > 0 ? urls : pubkey ? [...DEFAULT_FAVORITE_RELAYS] : []
if (!favoriteRelaysEvent) {
return useGlobal && pubkey ? [...DEFAULT_FAVORITE_RELAYS] : []
}
if (fromEvent.length > 0) return fromEvent
return useGlobal && pubkey ? [...DEFAULT_FAVORITE_RELAYS] : []
}
function blockedRelayUrlsFromEvent(blockedRelaysEvent: Event | null): string[] {
@ -1561,7 +1577,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1561,7 +1577,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
noteStatsService.beginPublishPriority()
try {
logger.debug('[Publish] Determining target relays...', { kind: event.kind, pubkey: event.pubkey?.substring(0, 8) })
const favoriteRelayUrls = favoriteRelayUrlsForPublish(favoriteRelaysEvent, account.pubkey)
const favoriteRelayUrls = favoriteRelayUrlsForPublish(favoriteRelaysEvent, account.pubkey, relayList)
const relays = await client.determineTargetRelays(event, {
...options,
favoriteRelayUrls,
@ -1686,7 +1702,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1686,7 +1702,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
client.interruptBackgroundQueries()
// Privacy: Only use user's own relays, never connect to "seen on" relays
const favUrls = favoriteRelayUrlsForPublish(favoriteRelaysEvent, account?.pubkey ?? null)
const favUrls = favoriteRelayUrlsForPublish(favoriteRelaysEvent, account?.pubkey ?? null, relayList)
const relays = await client.determineTargetRelays(targetEvent, {
favoriteRelayUrls: favUrls,
blockedRelayUrls: blockedRelayUrlsFromEvent(blockedRelaysEvent)

68
src/services/client.service.ts

@ -36,6 +36,8 @@ import { @@ -36,6 +36,8 @@ import {
SEARCHABLE_RELAY_URLS
} from '@/constants'
import { profileFetchRelayUrlsWithoutFastReadLayer, viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
/** NIP-01 filter keys only; NIP-50 adds `search` which non-searchable relays reject. */
function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter {
if (relaySupportsSearch) return f
@ -986,6 +988,14 @@ class ClientService extends EventTarget { @@ -986,6 +988,14 @@ class ClientService extends EventTarget {
blockedRelays: blockedRelayUrls,
applySocialKindBlockedFilter: isSocialKindBlockedKind(event.kind)
}
const policyRelayList = await this.peekRelayListFromStorage(event.pubkey).catch(() =>
this.emptyRelayListForPublish()
)
const useGlobalRelayDefaults = viewerUsesGlobalRelayDefaults({
viewerPubkey: event.pubkey,
favoriteRelayUrls: favoriteRelayUrls ?? [],
relayList: policyRelayList
})
if (event.kind === kinds.RelayList) {
logger.info('[DetermineTargetRelays] Determining target relays for relay list event', {
pubkey: event.pubkey,
@ -1023,11 +1033,24 @@ class ClientService extends EventTarget { @@ -1023,11 +1033,24 @@ class ClientService extends EventTarget {
}
if (userWriteRelays.length === 0 && seenRelays.length === 0) {
if (!useGlobalRelayDefaults) {
return this.filterPublishingRelays(
buildPrioritizedWriteRelayUrls({
userWriteRelays: [],
favoriteRelays: favoriteRelayUrls ?? [],
maxRelays: MAX_PUBLISH_RELAYS,
includeGlobalFastWriteReadTails: false,
...writeRelayPubOpts
}),
event
)
}
return this.filterPublishingRelays(
buildPrioritizedWriteRelayUrls({
userWriteRelays: [...FAST_WRITE_RELAY_URLS],
favoriteRelays: favoriteRelayUrls ?? [],
maxRelays: MAX_PUBLISH_RELAYS,
includeGlobalFastWriteReadTails: false,
...writeRelayPubOpts
}),
event
@ -1040,6 +1063,7 @@ class ClientService extends EventTarget { @@ -1040,6 +1063,7 @@ class ClientService extends EventTarget {
favoriteRelays: favoriteRelayUrls ?? [],
extraRelays: seenRelays,
maxRelays: MAX_PUBLISH_RELAYS,
includeGlobalFastWriteReadTails: useGlobalRelayDefaults,
...writeRelayPubOpts
}),
event
@ -1073,7 +1097,7 @@ class ClientService extends EventTarget { @@ -1073,7 +1097,7 @@ class ClientService extends EventTarget {
.filter((url): url is string => !!url)
let authorWrite = dedupeNormalizeRelayUrlsOrdered([...authorHttpWrites, ...authorWsWrites])
if (authorWrite.length === 0) {
authorWrite = [...FAST_WRITE_RELAY_URLS]
authorWrite = useGlobalRelayDefaults ? [...FAST_WRITE_RELAY_URLS] : []
}
let recipientRead: string[] = []
recipientRead = recipientRelayLists.flatMap((rl) => [
@ -1112,6 +1136,9 @@ class ClientService extends EventTarget { @@ -1112,6 +1136,9 @@ class ClientService extends EventTarget {
recipientReadCount: recipientRead.length
})
if (pubRelays.length > 0) return pubRelays
if (!useGlobalRelayDefaults) {
return this.filterPublishingRelays([], event)
}
return this.filterPublishingRelays(
feedRelayPolicyUrls([{ source: 'fast-write', urls: relayUrlsLocalsFirst([...FAST_WRITE_RELAY_URLS]) }], {
operation: 'write',
@ -1161,10 +1188,14 @@ class ClientService extends EventTarget { @@ -1161,10 +1188,14 @@ class ClientService extends EventTarget {
userWriteRelays:
spellWriteFiltered.length > 0
? spellWriteFiltered
: dedupeNormalizeRelayUrlsOrdered(FAST_WRITE_RELAY_URLS),
: useGlobalRelayDefaults
? dedupeNormalizeRelayUrlsOrdered(FAST_WRITE_RELAY_URLS)
: [],
favoriteRelays: favoriteRelayUrls ?? [],
extraRelays: [],
maxRelays: MAX_PUBLISH_RELAYS,
includeGlobalFastWriteReadTails:
spellWriteFiltered.length > 0 ? useGlobalRelayDefaults : false,
...writeRelayPubOpts
}),
event
@ -1203,23 +1234,33 @@ class ClientService extends EventTarget { @@ -1203,23 +1234,33 @@ class ClientService extends EventTarget {
ExtendedKind.RELAY_REVIEW
].includes(event.kind)
) {
bootstrapExtras.push(...PROFILE_FETCH_RELAY_URLS)
bootstrapExtras.push(
...(useGlobalRelayDefaults ? PROFILE_FETCH_RELAY_URLS : profileFetchRelayUrlsWithoutFastReadLayer())
)
logger.debug('[DetermineTargetRelays] Relay list event detected, adding PROFILE_FETCH_RELAY_URLS', {
kind: event.kind,
profileFetchRelays: PROFILE_FETCH_RELAY_URLS,
profileFetchRelays: useGlobalRelayDefaults
? PROFILE_FETCH_RELAY_URLS
: profileFetchRelayUrlsWithoutFastReadLayer(),
additionalRelayCount: bootstrapExtras.length
})
} else if (event.kind === ExtendedKind.FAVORITE_RELAYS || event.kind === kinds.Relaysets) {
// Use fast write relays for favorite-relays and kind 30002 relay-set replaceables to avoid
// timeouts and auth-only relays dominating the attempt list.
bootstrapExtras.push(...FAST_WRITE_RELAY_URLS)
if (useGlobalRelayDefaults) {
bootstrapExtras.push(...FAST_WRITE_RELAY_URLS)
}
logger.debug('[DetermineTargetRelays] Favorite relays or relay set event, adding FAST_WRITE_RELAY_URLS', {
kind: event.kind,
fastWriteRelays: FAST_WRITE_RELAY_URLS,
additionalRelayCount: bootstrapExtras.length
})
} else if (event.kind === ExtendedKind.RSS_FEED_LIST) {
bootstrapExtras.push(...FAST_WRITE_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS)
if (useGlobalRelayDefaults) {
bootstrapExtras.push(...FAST_WRITE_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS)
} else {
bootstrapExtras.push(...profileFetchRelayUrlsWithoutFastReadLayer())
}
}
if (isDocumentRelayKind(event.kind)) {
bootstrapExtras.push(...DOCUMENT_RELAY_URLS)
@ -1253,6 +1294,7 @@ class ClientService extends EventTarget { @@ -1253,6 +1294,7 @@ class ClientService extends EventTarget {
favoriteRelays: favoriteRelayUrls ?? [],
extraRelays: bootstrapExtras,
maxRelays: MAX_PUBLISH_RELAYS,
includeGlobalFastWriteReadTails: useGlobalRelayDefaults,
...writeRelayPubOpts
}),
event
@ -1275,12 +1317,14 @@ class ClientService extends EventTarget { @@ -1275,12 +1317,14 @@ class ClientService extends EventTarget {
// Fallback for all publishing when no relays (e.g. after cache clear or fetch failure).
// Use FAST_WRITE_RELAY_URLS so writes always have known-good write relays.
if (!relays.length) {
relays = isDocumentRelayKind(event.kind)
? dedupeNormalizeRelayUrlsOrdered([...FAST_WRITE_RELAY_URLS, ...DOCUMENT_RELAY_URLS])
: [...FAST_WRITE_RELAY_URLS]
logger.info('[DetermineTargetRelays] Using default write relays (no user/extra relays)', {
count: relays.length
})
if (useGlobalRelayDefaults) {
relays = isDocumentRelayKind(event.kind)
? dedupeNormalizeRelayUrlsOrdered([...FAST_WRITE_RELAY_URLS, ...DOCUMENT_RELAY_URLS])
: [...FAST_WRITE_RELAY_URLS]
logger.info('[DetermineTargetRelays] Using default write relays (no user/extra relays)', {
count: relays.length
})
}
}
relays = this.filterPublishingRelays(relays, event)

7
src/services/spell.service.ts

@ -87,11 +87,14 @@ export function getRelaysForSpellCatalogSync( @@ -87,11 +87,14 @@ export function getRelaysForSpellCatalogSync(
favoriteRelays: string[],
blockedRelays: string[],
userInboxReadRelays: string[],
options?: { userWriteRelays?: string[] }
options?: { userWriteRelays?: string[]; useGlobalRelayBootstrap?: boolean }
): string[] {
const g = options?.useGlobalRelayBootstrap !== false
return getRelayUrlsWithFavoritesFastReadAndInbox(favoriteRelays, blockedRelays, userInboxReadRelays, {
userWriteRelays: options?.userWriteRelays ?? [],
applySocialKindBlockedFilter: false
applySocialKindBlockedFilter: false,
useGlobalFavoriteDefaults: g,
includeGlobalFastRead: g
})
}

Loading…
Cancel
Save