Browse Source

fix relay feeds

imwald
Silberengel 1 month ago
parent
commit
0e987713c5
  1. 14
      src/components/NormalFeed/index.tsx
  2. 96
      src/components/NoteList/index.tsx
  3. 5
      src/components/ui/sonner.tsx
  4. 23
      src/constants.ts
  5. 2
      src/i18n/locales/de.ts
  6. 2
      src/i18n/locales/en.ts
  7. 39
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  8. 3
      src/providers/ThemeProvider.tsx
  9. 13
      src/services/client-query.service.ts
  10. 25
      src/services/client.service.ts

14
src/components/NormalFeed/index.tsx

@ -7,7 +7,7 @@ import storage from '@/services/local-storage.service'
import type { TPrimaryPageName } from '@/PageManager' import type { TPrimaryPageName } from '@/PageManager'
import { TFeedSubRequest, TNoteListMode } from '@/types' import { TFeedSubRequest, TNoteListMode } from '@/types'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react' import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import KindFilter from '../KindFilter' import KindFilter from '../KindFilter'
const NormalFeed = forwardRef<TNoteListRef, { const NormalFeed = forwardRef<TNoteListRef, {
@ -28,7 +28,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
mergeTimelineWhenSubRequestFiltersMatch?: boolean mergeTimelineWhenSubRequestFiltersMatch?: boolean
/** Home favorite-relays chip scope; see {@link NoteList} `feedTimelineScopeKey`. */ /** Home favorite-relays chip scope; see {@link NoteList} `feedTimelineScopeKey`. */
feedTimelineScopeKey?: string feedTimelineScopeKey?: string
/** Single-relay Explore / chip: kindless REQ (limit 200), no feed kind filter. */ /** Single-relay Explore / chip: kindless REQ (see `SINGLE_RELAY_KINDLESS_REQ_LIMIT` in constants), no feed kind filter. */
useFilterAsIs?: boolean useFilterAsIs?: boolean
clientSideKindFilter?: boolean clientSideKindFilter?: boolean
allowKindlessRelayExplore?: boolean allowKindlessRelayExplore?: boolean
@ -38,6 +38,10 @@ const NormalFeed = forwardRef<TNoteListRef, {
showFeedClientFilter?: boolean showFeedClientFilter?: boolean
/** When set, {@link NoteList} clears 🔍 filters when another primary tab is shown (mounted-but-hidden pages). */ /** When set, {@link NoteList} clears 🔍 filters when another primary tab is shown (mounted-but-hidden pages). */
hostPrimaryPageName?: TPrimaryPageName hostPrimaryPageName?: TPrimaryPageName
/** Single-relay kindless wave EOSEd with no events: parent re-subscribes with explicit kinds. */
onSingleRelayKindlessEmpty?: () => void
/** Shown above the feed list (e.g. after kindless→kinds fallback on a single-relay chip). */
feedTopNotice?: ReactNode
}>(function NormalFeed( }>(function NormalFeed(
{ {
subRequests, subRequests,
@ -53,7 +57,9 @@ const NormalFeed = forwardRef<TNoteListRef, {
clientSideKindFilter = false, clientSideKindFilter = false,
allowKindlessRelayExplore = false, allowKindlessRelayExplore = false,
showFeedClientFilter: showFeedClientFilterProp, showFeedClientFilter: showFeedClientFilterProp,
hostPrimaryPageName hostPrimaryPageName,
onSingleRelayKindlessEmpty,
feedTopNotice
}, },
ref ref
) { ) {
@ -217,6 +223,8 @@ const NormalFeed = forwardRef<TNoteListRef, {
showFeedClientFilter={showFeedClientFilter} showFeedClientFilter={showFeedClientFilter}
hostPrimaryPageName={hostPrimaryPageName} hostPrimaryPageName={hostPrimaryPageName}
feedClientFilterTabRowHost={mergeFilterWithTabsRow ? feedFilterTabRowHost : undefined} feedClientFilterTabRowHost={mergeFilterWithTabsRow ? feedFilterTabRowHost : undefined}
onSingleRelayKindlessEmpty={onSingleRelayKindlessEmpty}
feedTopNotice={feedTopNotice}
/> />
</div> </div>
</> </>

96
src/components/NoteList/index.tsx

@ -47,7 +47,8 @@ import {
useLayoutEffect, useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState useState,
type ReactNode
} from 'react' } from 'react'
import { CircleAlert } from 'lucide-react' import { CircleAlert } from 'lucide-react'
import { useLongPressAction } from '@/hooks/use-long-press-action' import { useLongPressAction } from '@/hooks/use-long-press-action'
@ -254,8 +255,9 @@ const NoteList = forwardRef(
*/ */
timelineLoadingSafetyTimeoutMs, timelineLoadingSafetyTimeoutMs,
/** /**
* With {@link useFilterAsIs}: omit relay `kinds` when the subrequest filter has none, and narrow * With {@link useFilterAsIs}: omit relay `kinds` when the subrequest filter has none. Kindless relay feeds
* incoming events to {@link showKinds} before merging (so caps are not filled by unrelated kinds). * merge the full batch; the kind picker still applies in the list via {@link applyKindPickerInUi}. Other
* `useFilterAsIs` paths may still narrow merged batches to {@link showKinds}.
*/ */
clientSideKindFilter = false, clientSideKindFilter = false,
/** /**
@ -293,7 +295,9 @@ const NoteList = forwardRef(
* When {@link NormalFeed} renders Notes/Replies + kind row, it passes the slot element so the 🔍 control * When {@link NormalFeed} renders Notes/Replies + kind row, it passes the slot element so the 🔍 control
* sits on that row instead of an extra bar above the list. Omitted on spells / standalone NoteList. * sits on that row instead of an extra bar above the list. Omitted on spells / standalone NoteList.
*/ */
feedClientFilterTabRowHost feedClientFilterTabRowHost,
onSingleRelayKindlessEmpty,
feedTopNotice
}: { }: {
subRequests: TFeedSubRequest[] subRequests: TFeedSubRequest[]
showKinds: number[] showKinds: number[]
@ -335,6 +339,10 @@ const NoteList = forwardRef(
showFeedClientFilter?: boolean showFeedClientFilter?: boolean
hostPrimaryPageName?: TPrimaryPageName hostPrimaryPageName?: TPrimaryPageName
feedClientFilterTabRowHost?: HTMLElement | null feedClientFilterTabRowHost?: HTMLElement | null
/** Single-relay kindless: if EOSE with no events, parent switches to explicit kinds in `subRequests`. */
onSingleRelayKindlessEmpty?: () => void
/** Optional banner above the feed (e.g. kindless→kinds fallback). */
feedTopNotice?: ReactNode
}, },
ref ref
) => { ) => {
@ -400,6 +408,10 @@ const NoteList = forwardRef(
const feedPaintLiveRelayDoneRef = useRef(false) const feedPaintLiveRelayDoneRef = useRef(false)
/** True if any timeline `onEvents` batch had `batch.length > 0`, or one-shot fetches returned any raw events (before UI filters). */ /** True if any timeline `onEvents` batch had `batch.length > 0`, or one-shot fetches returned any raw events (before UI filters). */
const feedRelayReturnedAnyEventRef = useRef(false) const feedRelayReturnedAnyEventRef = useRef(false)
/** One-shot per timeline init: avoid double-calling parent fallback (Strict Mode / duplicate EOSE). */
const singleRelayKindlessFallbackAttemptedRef = useRef(false)
const onSingleRelayKindlessEmptyRef = useRef(onSingleRelayKindlessEmpty)
onSingleRelayKindlessEmptyRef.current = onSingleRelayKindlessEmpty
/** Dedupe {@link toast.error} when relays return nothing for a feed load. */ /** Dedupe {@link toast.error} when relays return nothing for a feed load. */
const emptyRelayNoHitsToastKeyRef = useRef('') const emptyRelayNoHitsToastKeyRef = useRef('')
/** Per-relay outcomes for the current subscribe wave (merged shards); drives empty-feed toast detail. */ /** Per-relay outcomes for the current subscribe wave (merged shards); drives empty-feed toast detail. */
@ -605,8 +617,9 @@ const NoteList = forwardRef(
clientSideKindFilterRef.current = clientSideKindFilter clientSideKindFilterRef.current = clientSideKindFilter
/** /**
* When to apply kind picker + kind-1/1111/GitRelease visibility to rows. Kindless home relay chips use a * When to apply kind picker + kind-1/1111/GitRelease visibility to visible rows. Kindless relay REQs merge
* kindless REQ and narrow here via {@link clientSideKindFilter}; standalone relay explore keeps firehose. * the full relay batch; this still filters what the list shows (unlike standalone relay explore, which sets
* {@link allowKindlessRelayExplore} without {@link clientSideKindFilter} and shows the firehose).
*/ */
const applyKindPickerInUi = useMemo( const applyKindPickerInUi = useMemo(
() => () =>
@ -1184,6 +1197,7 @@ const NoteList = forwardRef(
feedPaintRelayMetaRef.current = null feedPaintRelayMetaRef.current = null
feedPaintLiveRelayDoneRef.current = false feedPaintLiveRelayDoneRef.current = false
feedRelayReturnedAnyEventRef.current = false feedRelayReturnedAnyEventRef.current = false
singleRelayKindlessFallbackAttemptedRef.current = false
// Re-subscribe with rows visible (e.g. relay URL expansion): don't flash global loading / skeleton. // Re-subscribe with rows visible (e.g. relay URL expansion): don't flash global loading / skeleton.
const keepRowsVisible = const keepRowsVisible =
@ -1289,18 +1303,16 @@ const NoteList = forwardRef(
return undefined return undefined
} }
/**
* Kindless relay REQ (`allowKindlessRelayExplore`): never drop events here relays return many kinds;
* merging only rows in {@link showKinds} left almost nothing in the timeline (e.g. christpill 200 events 1
* visible) while relay explore showed the full firehose. {@link applyKindPickerInUi} / {@link filteredEvents}
* still apply the kind picker for what the user sees.
*/
const narrowLiveBatch = (evs: Event[]) => { const narrowLiveBatch = (evs: Event[]) => {
if (seeAllFeedEventsRef.current) return evs if (seeAllFeedEventsRef.current) return evs
if ( if (allowKindlessRelayExploreRef.current) return evs
allowKindlessRelayExploreRef.current && if (!useFilterAsIsRef.current || !clientSideKindFilterRef.current) return evs
!(useFilterAsIsRef.current && clientSideKindFilterRef.current)
) {
return evs
}
if (!useFilterAsIsRef.current || !clientSideKindFilterRef.current) {
if (!allowKindlessRelayExploreRef.current) return evs
return evs
}
return evs.filter((e) => showKinds.includes(e.kind)) return evs.filter((e) => showKinds.includes(e.kind))
} }
@ -1536,15 +1548,38 @@ const NoteList = forwardRef(
setHasMore(true) setHasMore(true)
} }
} }
// Single-relay home chip: kindless REQ returned nothing — parent re-subscribes with explicit kinds.
if (
eosed &&
effectActive &&
onSingleRelayKindlessEmptyRef.current &&
!singleRelayKindlessFallbackAttemptedRef.current &&
!feedRelayReturnedAnyEventRef.current
) {
const reqs = subRequestsRef.current
const f0 = reqs[0]
if (
reqs.length === 1 &&
f0 &&
f0.urls.length === 1 &&
allowKindlessRelayExploreRef.current &&
useFilterAsIsRef.current &&
clientSideKindFilterRef.current
) {
const f = f0.filter as Filter
const noKinds = !f.kinds || f.kinds.length === 0
if (noKinds) {
singleRelayKindlessFallbackAttemptedRef.current = true
onSingleRelayKindlessEmptyRef.current()
}
}
}
}, },
onNew: (event: Event) => { onNew: (event: Event) => {
if (!effectActive) return if (!effectActive) return
feedRelayReturnedAnyEventRef.current = true feedRelayReturnedAnyEventRef.current = true
if ( if (!seeAllFeedEventsRef.current && !allowKindlessRelayExploreRef.current) {
!seeAllFeedEventsRef.current &&
(!allowKindlessRelayExploreRef.current ||
(useFilterAsIsRef.current && clientSideKindFilterRef.current))
) {
if (!useFilterAsIsRef.current && !showKinds.includes(event.kind)) return if (!useFilterAsIsRef.current && !showKinds.includes(event.kind)) return
if ( if (
clientSideKindFilterRef.current && clientSideKindFilterRef.current &&
@ -1657,7 +1692,8 @@ const NoteList = forwardRef(
oneShotEoseTimeoutMs, oneShotEoseTimeoutMs,
oneShotFirstRelayGraceMs, oneShotFirstRelayGraceMs,
clientSideKindFilter, clientSideKindFilter,
allowKindlessRelayExplore allowKindlessRelayExplore,
onSingleRelayKindlessEmpty
]) ])
const oneShotDebugPrevLoadingRef = useRef(false) const oneShotDebugPrevLoadingRef = useRef(false)
@ -2461,12 +2497,28 @@ const NoteList = forwardRef(
pullingContent="" pullingContent=""
> >
<div> <div>
{feedTopNotice ? (
<div
className="mb-2 rounded-md border border-border/80 bg-muted/35 px-3 py-2 text-sm text-muted-foreground"
role="note"
>
{feedTopNotice}
</div>
) : null}
{showFeedClientFilter ? feedClientFilterBar : null} {showFeedClientFilter ? feedClientFilterBar : null}
{list} {list}
</div> </div>
</PullToRefresh> </PullToRefresh>
) : ( ) : (
<div> <div>
{feedTopNotice ? (
<div
className="mb-2 rounded-md border border-border/80 bg-muted/35 px-3 py-2 text-sm text-muted-foreground"
role="note"
>
{feedTopNotice}
</div>
) : null}
{showFeedClientFilter ? feedClientFilterBar : null} {showFeedClientFilter ? feedClientFilterBar : null}
{list} {list}
</div> </div>

5
src/components/ui/sonner.tsx

@ -1,10 +1,11 @@
import { useTheme } from '@/providers/ThemeProvider' import { useThemeOptional } from '@/providers/ThemeProvider'
import { Toaster as Sonner } from 'sonner' import { Toaster as Sonner } from 'sonner'
type ToasterProps = React.ComponentProps<typeof Sonner> type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { themeSetting } = useTheme() const themeCtx = useThemeOptional()
const themeSetting = themeCtx?.themeSetting ?? 'system'
return ( return (
<Sonner <Sonner

23
src/constants.ts

@ -101,7 +101,7 @@ export const FEED_FIRST_RELAY_RESULT_GRACE_MIN_LIMIT = 200
/** /**
* Kindless single-relay page REQ: explicit `limit`, no `kinds` (see NoteList `allowKindlessRelayExplore`). * Kindless single-relay page REQ: explicit `limit`, no `kinds` (see NoteList `allowKindlessRelayExplore`).
*/ */
export const SINGLE_RELAY_KINDLESS_REQ_LIMIT = 200 export const SINGLE_RELAY_KINDLESS_REQ_LIMIT = 500
/** /**
* Minimum time between full account network hydrates (NostrProvider: relay + replaceable fetch from relays). * Minimum time between full account network hydrates (NostrProvider: relay + replaceable fetch from relays).
@ -249,6 +249,13 @@ export const READ_ONLY_RELAY_URLS = [
'wss://relay.nip46.com' 'wss://relay.nip46.com'
] ]
/**
* Relays that need NIP-42 signed before the first REQ returns useful data. Same pool treatment as
* {@link READ_ONLY_RELAY_URLS} (longer connect timeout + proactive `automaticallyAuth`), but **not**
* necessarily read-only for publish keep those relays out of {@link READ_ONLY_RELAY_URLS}.
*/
export const NIP42_POOL_AUTOMATIC_AUTH_RELAY_URLS = ['wss://nostr.wine'] as const
/** /**
* Relays that reject or poorly serve social kinds (short notes, discussions, URL comments). * Relays that reject or poorly serve social kinds (short notes, discussions, URL comments).
* Strip these from REQ/publish relay stacks when the filter or event uses {@link SOCIAL_KIND_BLOCKED_KINDS}, * Strip these from REQ/publish relay stacks when the filter or event uses {@link SOCIAL_KIND_BLOCKED_KINDS},
@ -469,6 +476,20 @@ export function relayFilterIncludesSocialKindBlockedKind(filter: Filter): boolea
return arr.some((kind) => SOCIAL_KIND_BLOCKED_KIND_SET.has(kind)) return arr.some((kind) => SOCIAL_KIND_BLOCKED_KIND_SET.has(kind))
} }
/**
* After dropping {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} from a relay stack: if every URL was removed but the caller
* passed exactly one relay (e.g. a favorite-relay chip), keep it. Blended stacks still omit these relays; a
* user-targeted single-relay feed should actually contact that relay (e.g. thecitadel for kinds the relay does carry).
*/
export function relaysAfterSocialKindBlockedStrip(
originalDedupedUrls: string[],
afterStrip: string[]
): string[] {
if (afterStrip.length > 0) return afterStrip
if (originalDedupedUrls.length === 1) return [...originalDedupedUrls]
return afterStrip
}
/** Event kinds that show “Read this note aloud” in note options (Web Speech API). */ /** Event kinds that show “Read this note aloud” in note options (Web Speech API). */
export const READ_ALOUD_KINDS: readonly number[] = [ export const READ_ALOUD_KINDS: readonly number[] = [
kinds.ShortTextNote, kinds.ShortTextNote,

2
src/i18n/locales/de.ts

@ -649,6 +649,8 @@ export default {
None: 'Keine', None: 'Keine',
'Cache & offline storage': 'Cache & Offline-Speicher', 'Cache & offline storage': 'Cache & Offline-Speicher',
feedStarting: 'Starting feeds and relays… This can take a few seconds after login.', feedStarting: 'Starting feeds and relays… This can take a few seconds after login.',
singleRelayKindFallbackNotice:
'Dieses Relay hat auf eine offene Anfrage (ohne kinds im Filter) keine Events geliefert. Der Feed unten nutzt stattdessen deinen gewohnten Kind-Filter.',
refreshCacheButtonExplainer: refreshCacheButtonExplainer:
'Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.', 'Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.',
'eventArchive.sectionTitle': 'Notes & feed archive', 'eventArchive.sectionTitle': 'Notes & feed archive',

2
src/i18n/locales/en.ts

@ -638,6 +638,8 @@ export default {
None: 'None', None: 'None',
'Cache & offline storage': 'Cache & offline storage', 'Cache & offline storage': 'Cache & offline storage',
feedStarting: 'Starting feeds and relays… This can take a few seconds after login.', feedStarting: 'Starting feeds and relays… This can take a few seconds after login.',
singleRelayKindFallbackNotice:
'This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.',
refreshCacheButtonExplainer: refreshCacheButtonExplainer:
'Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.', 'Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.',
'eventArchive.sectionTitle': 'Notes & feed archive', 'eventArchive.sectionTitle': 'Notes & feed archive',

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

@ -7,7 +7,8 @@ import { useFeed } from '@/providers/FeedProvider'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider' import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import relayInfoService from '@/services/relay-info.service' import relayInfoService from '@/services/relay-info.service'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import React, { forwardRef, useEffect, useMemo, useState } from 'react' import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
const RelaysFeed = forwardRef< const RelaysFeed = forwardRef<
TNoteListRef, TNoteListRef,
@ -18,10 +19,13 @@ const RelaysFeed = forwardRef<
kindsOverride?: number[] kindsOverride?: number[]
} }
>(function RelaysFeed({ setSubHeader, onSubHeaderRefresh, kindsOverride }, ref) { >(function RelaysFeed({ setSubHeader, onSubHeaderRefresh, kindsOverride }, ref) {
const { t } = useTranslation()
const { feedInfo, relayUrls } = useFeed() const { feedInfo, relayUrls } = useFeed()
const { showKinds } = useKindFilterOrDefaults() const { showKinds } = useKindFilterOrDefaults()
const [areAlgoRelays, setAreAlgoRelays] = useState(false) const [areAlgoRelays, setAreAlgoRelays] = useState(false)
const [relayAlgoReady, setRelayAlgoReady] = useState(false) const [relayAlgoReady, setRelayAlgoReady] = useState(false)
/** After kindless single-relay REQ EOSEs with no events, re-subscribe with the normal kind list. */
const [singleRelayKindFallback, setSingleRelayKindFallback] = useState(false)
const relayUrlsKey = useMemo( const relayUrlsKey = useMemo(
() => () =>
@ -76,10 +80,6 @@ const RelaysFeed = forwardRef<
? showKinds ? showKinds
: [kinds.ShortTextNote] : [kinds.ShortTextNote]
/** One relay + user kind filter: avoid huge `kinds` REQ (many relays error with "too many kinds"). */
const singleRelayKindlessExplore =
feedInfo.feedType === 'relay' && relayUrls.length === 1 && !kindsOverride?.length
const canRenderFeed = const canRenderFeed =
(feedInfo.feedType === 'relay' || (feedInfo.feedType === 'relay' ||
feedInfo.feedType === 'relays' || feedInfo.feedType === 'relays' ||
@ -97,6 +97,29 @@ const RelaysFeed = forwardRef<
return undefined return undefined
}, [feedInfo.feedType, feedInfo.id]) }, [feedInfo.feedType, feedInfo.id])
/** New relay chip / set: try kindless first again. */
useEffect(() => {
setSingleRelayKindFallback(false)
}, [feedTimelineScopeKey])
const onSingleRelayKindlessEmpty = useCallback(() => {
setSingleRelayKindFallback(true)
}, [])
/**
* One relay + user kind filter: kindless `{ limit }` REQ first (many relays error on huge `kinds` arrays).
* If that EOSEs with no events, `onSingleRelayKindlessEmpty` switches to explicit `kinds`.
*/
const singleRelayKindlessExplore =
feedInfo.feedType === 'relay' &&
relayUrls.length === 1 &&
!kindsOverride?.length &&
!singleRelayKindFallback
const feedTopNotice = singleRelayKindFallback ? (
<p className="leading-snug">{t('singleRelayKindFallbackNotice')}</p>
) : null
// Hooks must run every render — never place useMemo after conditional returns. // Hooks must run every render — never place useMemo after conditional returns.
const subRequests = useMemo(() => { const subRequests = useMemo(() => {
if (!canRenderFeed) return [] if (!canRenderFeed) return []
@ -136,6 +159,12 @@ const RelaysFeed = forwardRef<
clientSideKindFilter={singleRelayKindlessExplore} clientSideKindFilter={singleRelayKindlessExplore}
showFeedClientFilter showFeedClientFilter
hostPrimaryPageName="feed" hostPrimaryPageName="feed"
onSingleRelayKindlessEmpty={
feedInfo.feedType === 'relay' && relayUrls.length === 1 && !kindsOverride?.length
? onSingleRelayKindlessEmpty
: undefined
}
feedTopNotice={feedTopNotice}
/> />
) )
}) })

3
src/providers/ThemeProvider.tsx

@ -89,3 +89,6 @@ export const useTheme = () => {
return context return context
} }
/** For leaf UI (e.g. Toaster) during Vite HMR when the tree can briefly mount outside ThemeProvider. */
export const useThemeOptional = (): ThemeProviderState | undefined => useContext(ThemeProviderContext)

13
src/services/client-query.service.ts

@ -2,6 +2,7 @@ import {
FEED_FIRST_RELAY_RESULT_GRACE_MIN_LIMIT, FEED_FIRST_RELAY_RESULT_GRACE_MIN_LIMIT,
FIRST_RELAY_RESULT_GRACE_MS, FIRST_RELAY_RESULT_GRACE_MS,
relayFilterIncludesSocialKindBlockedKind, relayFilterIncludesSocialKindBlockedKind,
relaysAfterSocialKindBlockedStrip,
SOCIAL_KIND_BLOCKED_RELAY_URLS, SOCIAL_KIND_BLOCKED_RELAY_URLS,
MAX_CONCURRENT_RELAY_CONNECTIONS, MAX_CONCURRENT_RELAY_CONNECTIONS,
MAX_CONCURRENT_SUBS_PER_RELAY, MAX_CONCURRENT_SUBS_PER_RELAY,
@ -447,7 +448,8 @@ export class QueryService {
callbacks: SubscribeCallbacks, callbacks: SubscribeCallbacks,
relayOpMeta?: { source: string; logLevel?: 'info' | 'debug' } relayOpMeta?: { source: string; logLevel?: 'info' | 'debug' }
): { close: () => void } { ): { close: () => void } {
let relays = Array.from(new Set(urls)) const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays
const filters = Array.isArray(filter) ? filter : [filter] const filters = Array.isArray(filter) ? filter : [filter]
const stripSocialBlockedRelays = const stripSocialBlockedRelays =
@ -455,7 +457,8 @@ export class QueryService {
filters.some((f) => relayFilterIncludesSocialKindBlockedKind(f)) filters.some((f) => relayFilterIncludesSocialKindBlockedKind(f))
if (stripSocialBlockedRelays) { if (stripSocialBlockedRelays) {
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
relays = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url))
relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped)
} }
if (this.shouldSkipRelayForSession) { if (this.shouldSkipRelayForSession) {
relays = relays.filter((url) => { relays = relays.filter((url) => {
@ -686,7 +689,8 @@ export class QueryService {
onevent?: (evt: NEvent) => void onevent?: (evt: NEvent) => void
} & QueryOptions } & QueryOptions
): Promise<NEvent[]> { ): Promise<NEvent[]> {
let relays = Array.from(new Set(urls)) const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays
if (relays.length === 0) { if (relays.length === 0) {
const { FAST_READ_RELAY_URLS } = await import('@/constants') const { FAST_READ_RELAY_URLS } = await import('@/constants')
relays = [...FAST_READ_RELAY_URLS] relays = [...FAST_READ_RELAY_URLS]
@ -697,7 +701,8 @@ export class QueryService {
filters.some((f) => relayFilterIncludesSocialKindBlockedKind(f)) filters.some((f) => relayFilterIncludesSocialKindBlockedKind(f))
if (stripSocialBlockedRelays) { if (stripSocialBlockedRelays) {
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
relays = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url))
relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped)
} }
const { onevent, ...queryOpts } = options ?? {} const { onevent, ...queryOpts } = options ?? {}
return this.query(relays, filter, onevent, queryOpts) return this.query(relays, filter, onevent, queryOpts)

25
src/services/client.service.ts

@ -5,6 +5,7 @@ import {
FIRST_RELAY_RESULT_GRACE_MS, FIRST_RELAY_RESULT_GRACE_MS,
isSocialKindBlockedKind, isSocialKindBlockedKind,
relayFilterIncludesSocialKindBlockedKind, relayFilterIncludesSocialKindBlockedKind,
relaysAfterSocialKindBlockedStrip,
SOCIAL_KIND_BLOCKED_RELAY_URLS, SOCIAL_KIND_BLOCKED_RELAY_URLS,
MAX_PUBLISH_RELAYS, MAX_PUBLISH_RELAYS,
RELAY_POOL_CONNECTION_TIMEOUT_MS, RELAY_POOL_CONNECTION_TIMEOUT_MS,
@ -14,6 +15,7 @@ import {
NIP66_DISCOVERY_RELAY_URLS, NIP66_DISCOVERY_RELAY_URLS,
PROFILE_FETCH_RELAY_URLS, PROFILE_FETCH_RELAY_URLS,
READ_ONLY_RELAY_URLS, READ_ONLY_RELAY_URLS,
NIP42_POOL_AUTOMATIC_AUTH_RELAY_URLS,
SEARCHABLE_RELAY_URLS SEARCHABLE_RELAY_URLS
} from '@/constants' } from '@/constants'
@ -128,7 +130,9 @@ function summarizeFiltersForRelayLog(filters: Filter[]): Record<string, unknown>
} }
const READ_ONLY_RELAY_CONNECT_BOOST_URLS = new Set( const READ_ONLY_RELAY_CONNECT_BOOST_URLS = new Set(
READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u) [...READ_ONLY_RELAY_URLS, ...NIP42_POOL_AUTOMATIC_AUTH_RELAY_URLS].map(
(u) => normalizeUrl(u) || u
)
) )
/** Hostname (+ path when not "/") for readable publish / retry console lines. */ /** Hostname (+ path when not "/") for readable publish / retry console lines. */
@ -324,9 +328,10 @@ class ClientService extends EventTarget {
this.signerType = signerType this.signerType = signerType
this.queryService.setSigner(signer, signerType) this.queryService.setSigner(signer, signerType)
/** /**
* NIP-42: answer `AUTH` on the wire only for read-only aggregators (`READ_ONLY_RELAY_URLS`, e.g. aggr). * NIP-42: proactive `AUTH` for relays that need it before the first REQ (read-only aggregators +
* They often require AUTH before REQ; `master`-style auth only on `CLOSED` is too late. Other relays stay * {@link NIP42_POOL_AUTOMATIC_AUTH_RELAY_URLS}). Without this, a REQ can EOSE empty while the extension
* on reactive `relay.auth()` after `auth-required` to avoid double-sign races with the wider pool. * is still signing; the batch then finishes and never refetches. Other relays stay on reactive
* `relay.auth()` after `auth-required` to avoid double-sign races with the wider pool.
*/ */
if (signer && signerType !== 'npub') { if (signer && signerType !== 'npub') {
this.pool.automaticallyAuth = (relayURL: string) => { this.pool.automaticallyAuth = (relayURL: string) => {
@ -1812,7 +1817,8 @@ class ClientService extends EventTarget {
}, },
relayReqLog?: { groupId?: string; onBatchEnd?: (rows: RelayOpTerminalRow[]) => void } relayReqLog?: { groupId?: string; onBatchEnd?: (rows: RelayOpTerminalRow[]) => void }
) { ) {
let relays = Array.from(new Set(urls)) const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays
const filters = Array.isArray(filter) ? filter : [filter] const filters = Array.isArray(filter) ? filter : [filter]
const stripSocialBlockedRelays = const stripSocialBlockedRelays =
@ -1820,7 +1826,8 @@ class ClientService extends EventTarget {
filters.some((f) => relayFilterIncludesSocialKindBlockedKind(f)) filters.some((f) => relayFilterIncludesSocialKindBlockedKind(f))
if (stripSocialBlockedRelays) { if (stripSocialBlockedRelays) {
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
relays = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url))
relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped)
} }
relays = this.relayUrlsAfterStrikesOrRecover(relays) relays = this.relayUrlsAfterStrikesOrRecover(relays)
@ -2481,7 +2488,8 @@ class ClientService extends EventTarget {
immediateReturn?: boolean immediateReturn?: boolean
} = {} } = {}
) { ) {
let relays = Array.from(new Set(urls)) const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays
if (relays.length === 0) relays = [...FAST_READ_RELAY_URLS] if (relays.length === 0) relays = [...FAST_READ_RELAY_URLS]
const filters = Array.isArray(filter) ? filter : [filter] const filters = Array.isArray(filter) ? filter : [filter]
const stripSocialBlockedRelays = const stripSocialBlockedRelays =
@ -2489,7 +2497,8 @@ class ClientService extends EventTarget {
filters.some((f) => relayFilterIncludesSocialKindBlockedKind(f)) filters.some((f) => relayFilterIncludesSocialKindBlockedKind(f))
if (stripSocialBlockedRelays) { if (stripSocialBlockedRelays) {
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
relays = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url))
relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped)
} }
relays = this.relayUrlsAfterStrikesOrRecover(relays) relays = this.relayUrlsAfterStrikesOrRecover(relays)
const events = await this.queryService.query(relays, filter, onevent, { const events = await this.queryService.query(relays, filter, onevent, {

Loading…
Cancel
Save