- ) : reactionDisplay.status === 'vote_up' ? (
+ {reactionDisplay.status === 'vote_up' ? (
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index 9446532e..3d4eb1ed 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -76,7 +76,6 @@ import NotificationEventCard from './NotificationEventCard'
import ReactionEmojiDisplay from './ReactionEmojiDisplay'
import UnknownNote from './UnknownNote'
import NoteKindLabel from './NoteKindLabel'
-import { Skeleton } from '@/components/ui/skeleton'
import { Button } from '@/components/ui/button'
import VideoNote from './VideoNote'
import RelayReview from './RelayReview'
@@ -607,12 +606,7 @@ export default function Note({
{isNip25ReactionKind(event.kind) ? (
- {reactionDisplay.status === 'pending' ? (
-
- ) : reactionDisplay.status === 'vote_up' ? (
+ {reactionDisplay.status === 'vote_up' ? (
- {reactionDisplay.status === 'pending' ? (
-
- ) : reactionDisplay.status === 'vote_up' ? (
+ {reactionDisplay.status === 'vote_up' ? (
{DISCUSSION_UPVOTE_DISPLAY}
diff --git a/src/constants.ts b/src/constants.ts
index 2770582f..467a970d 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -469,7 +469,10 @@ export const SEARCHABLE_RELAY_URLS = [
export const PROFILE_RELAY_URLS = [
'wss://profiles.nostr1.com',
- 'wss://purplepag.es'
+ 'wss://purplepag.es',
+ 'wss://relay.primal.net',
+ 'wss://relay.damus.io',
+ 'wss://nos.lol'
]
export const FOLLOWS_HISTORY_RELAY_URLS = [
diff --git a/src/features/feed/relay-policy.test.ts b/src/features/feed/relay-policy.test.ts
index 5f1fa8a6..9ad6b0ea 100644
--- a/src/features/feed/relay-policy.test.ts
+++ b/src/features/feed/relay-policy.test.ts
@@ -5,7 +5,12 @@ describe('applyFeedRelayPolicy', () => {
it('prepends aggr.nostr.land for read feeds before caps', () => {
const result = applyFeedRelayPolicy(
[{ source: 'viewer-read', urls: ['wss://reader-a.example/', 'wss://reader-b.example/'] }],
- { operation: 'read', maxRelays: 2, applySocialKindBlockedFilter: false }
+ {
+ operation: 'read',
+ maxRelays: 2,
+ applySocialKindBlockedFilter: false,
+ nostrLandAggrEligible: true
+ }
)
expect(result.urls).toEqual(['wss://aggr.nostr.land/', 'wss://reader-a.example/'])
@@ -33,7 +38,8 @@ describe('applyFeedRelayPolicy', () => {
{
operation: 'read',
blockedRelays: ['wss://aggr.nostr.land/'],
- applySocialKindBlockedFilter: false
+ applySocialKindBlockedFilter: false,
+ nostrLandAggrEligible: true
}
)
diff --git a/src/features/feed/relay-policy.ts b/src/features/feed/relay-policy.ts
index fd138663..50ed3a8a 100644
--- a/src/features/feed/relay-policy.ts
+++ b/src/features/feed/relay-policy.ts
@@ -4,6 +4,7 @@ import {
relayFilterIncludesSocialKindBlockedKind
} from '@/constants'
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
+import { getViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility'
import {
relayFiltersUseCapitalLetterTagKeys,
relayUrlsStripExtendedTagReqBlocked
@@ -66,10 +67,16 @@ export type FeedRelayPolicyContext = {
eventKind?: number
maxRelays?: number
/**
- * Default: read surfaces include aggr.nostr.land; favorites and write
- * surfaces do not. Set explicitly for specialized fetches.
+ * Default: for `operation === 'read'`, prepend {@link AGGR_NOSTR_LAND_WSS} only when the viewer has a
+ * `nostr.land` host in their relay stack (see {@link getViewerRelayStackNostrLandAggrEligible}) or this
+ * flag is set true. `favorites-feed` never prepends. Use `nostrLandAggr: 'always'|'never'` to override.
*/
nostrLandAggr?: 'default' | 'always' | 'never'
+ /**
+ * Per-call override for read-surface aggr eligibility. When omitted, uses the global synced flag from
+ * {@link syncViewerRelayStackNostrLandAggrEligible}.
+ */
+ nostrLandAggrEligible?: boolean
applySocialKindBlockedFilter?: boolean
applyExtendedTagBlockedFilter?: boolean
preserveSingleExplicitRelay?: boolean
@@ -105,10 +112,17 @@ function shouldApplyExtendedTagFilter(ctx: FeedRelayPolicyContext): boolean {
return (ctx.filters ?? []).some((filter) => relayFiltersUseCapitalLetterTagKeys([filter]))
}
+function nostrLandAggrEligibleEffective(ctx: FeedRelayPolicyContext): boolean {
+ if (ctx.nostrLandAggrEligible !== undefined) return ctx.nostrLandAggrEligible
+ return getViewerRelayStackNostrLandAggrEligible()
+}
+
function shouldEnsureAggr(ctx: FeedRelayPolicyContext): boolean {
if (ctx.nostrLandAggr === 'always') return true
if (ctx.nostrLandAggr === 'never') return false
- return ctx.operation === 'read'
+ if (ctx.operation === 'favorites-feed') return false
+ if (ctx.operation === 'read') return nostrLandAggrEligibleEffective(ctx)
+ return false
}
function isReadOnlyRelay(norm: string): boolean {
diff --git a/src/hooks/useFetchProfile.tsx b/src/hooks/useFetchProfile.tsx
index 26e52d19..c3129c34 100644
--- a/src/hooks/useFetchProfile.tsx
+++ b/src/hooks/useFetchProfile.tsx
@@ -211,10 +211,7 @@ export function useFetchProfile(id?: string, skipCache = false) {
try {
globalFetchingPubkeys.add(pubkey)
- const quick = tryHydrateProfileFromSessionOnly(pubkey, skipCache)
- if (quick) {
- return quick
- }
+ /** Session-only fast path removed: {@link replaceableEventService.fetchProfileEvent} still refreshes from relays while session primes the loader. */
/** Disk read runs in parallel with `fetchProfileEvent` — never block network on IDB. */
idbEarlyP = profileFromIdbPromise(pubkey, skipCache)
@@ -516,8 +513,16 @@ export function useFetchProfile(id?: string, skipCache = false) {
const run = async () => {
try {
- setIsFetching(true)
setError(null)
+ const earlyProfile =
+ tryHydrateProfileFromSessionOnly(extractedPubkey, skipCache) ??
+ (await profileFromIdbPromise(extractedPubkey, skipCache))
+ if (!cancelled.current && earlyProfile) {
+ setProfile(earlyProfile)
+ setIsFetching(false)
+ } else if (!cancelled.current) {
+ setIsFetching(true)
+ }
const profile = await checkProfile(extractedPubkey, cancelled)
diff --git a/src/hooks/useNotificationReactionDisplay.ts b/src/hooks/useNotificationReactionDisplay.ts
index d4101372..7b34e232 100644
--- a/src/hooks/useNotificationReactionDisplay.ts
+++ b/src/hooks/useNotificationReactionDisplay.ts
@@ -7,19 +7,52 @@ import { getRootEventHexId } from '@/lib/event'
import { relayHintsFromEventTags } from '@/lib/relay-list-builder'
import { getFirstHexEventIdFromETags } from '@/lib/tag'
import { eventService } from '@/services/client.service'
+import type { NEvent } from '@/types'
import { Event, kinds } from 'nostr-tools'
-import { useEffect, useMemo, useState } from 'react'
+import { useEffect, useLayoutEffect, useMemo, useState } from 'react'
export type NotificationReactionDisplay =
- | { status: 'pending' }
| { status: 'vote_up' }
| { status: 'vote_down' }
| { status: 'discussion_custom' }
| { status: 'default' }
+function classifyDiscussionReactionFromTargets(
+ reaction: Event,
+ target: NEvent,
+ root: NEvent | undefined
+): NotificationReactionDisplay {
+ let inDiscussion = target.kind === ExtendedKind.DISCUSSION
+ if (!inDiscussion && target.kind === ExtendedKind.COMMENT) {
+ inDiscussion = root?.kind === ExtendedKind.DISCUSSION
+ }
+ if (!inDiscussion) return { status: 'default' }
+ const raw = reaction.content?.trim() ?? ''
+ if (isDiscussionUpvoteEmoji(raw)) return { status: 'vote_up' }
+ if (isDiscussionDownvoteEmoji(raw)) return { status: 'vote_down' }
+ return { status: 'discussion_custom' }
+}
+
+function peekReactionDisplayFromSessionCaches(event: Event): NotificationReactionDisplay {
+ if (event.kind !== kinds.Reaction) return { status: 'default' }
+ const targetId = getFirstHexEventIdFromETags(event.tags)
+ if (!targetId) return { status: 'default' }
+ const target = eventService.peekHexIdNoteFromSessionCache(targetId)
+ if (!target) return { status: 'default' }
+ let root: NEvent | undefined
+ if (target.kind === ExtendedKind.COMMENT) {
+ const rootId = getRootEventHexId(target)
+ if (rootId) root = eventService.peekHexIdNoteFromSessionCache(rootId)
+ }
+ return classifyDiscussionReactionFromTargets(event, target, root)
+}
+
/**
* For kind 7: resolves whether the reacted-to note is a discussion (kind 11 or 1111 under 11)
* and classifies +/- / ⬆️⬇️ as vote display vs other reactions.
+ *
+ * Always starts from session cache (sync) so the glyph is never a blank skeleton; async fetch refines
+ * when the target was not yet in memory.
*/
export function useNotificationReactionDisplay(event: Event): NotificationReactionDisplay {
const targetId = useMemo(() => {
@@ -29,9 +62,11 @@ export function useNotificationReactionDisplay(event: Event): NotificationReacti
const reactionRelayHints = useMemo(() => relayHintsFromEventTags(event), [event])
- const [state, setState] = useState(() =>
- event.kind === kinds.Reaction ? { status: 'pending' } : { status: 'default' }
- )
+ const [state, setState] = useState({ status: 'default' })
+
+ useLayoutEffect(() => {
+ setState(peekReactionDisplayFromSessionCaches(event))
+ }, [event.id, event.kind, event.content, event.tags])
useEffect(() => {
if (event.kind === ExtendedKind.EXTERNAL_REACTION) {
@@ -48,8 +83,6 @@ export function useNotificationReactionDisplay(event: Event): NotificationReacti
}
let cancelled = false
- setState({ status: 'pending' })
-
const fetchOpts = reactionRelayHints.length ? { relayHints: reactionRelayHints } : undefined
;(async () => {
@@ -60,13 +93,14 @@ export function useNotificationReactionDisplay(event: Event): NotificationReacti
return
}
+ let root: NEvent | undefined
let inDiscussion = target.kind === ExtendedKind.DISCUSSION
if (!inDiscussion && target.kind === ExtendedKind.COMMENT) {
const rootId = getRootEventHexId(target)
if (rootId) {
const rootHints = relayHintsFromEventTags(target)
const rootOpts = rootHints.length ? { relayHints: rootHints } : fetchOpts
- const root = await eventService.fetchEvent(rootId, rootOpts)
+ root = await eventService.fetchEvent(rootId, rootOpts)
if (cancelled) return
inDiscussion = root?.kind === ExtendedKind.DISCUSSION
}
@@ -77,14 +111,7 @@ export function useNotificationReactionDisplay(event: Event): NotificationReacti
return
}
- const raw = event.content?.trim() ?? ''
- if (isDiscussionUpvoteEmoji(raw)) {
- setState({ status: 'vote_up' })
- } else if (isDiscussionDownvoteEmoji(raw)) {
- setState({ status: 'vote_down' })
- } else {
- setState({ status: 'discussion_custom' })
- }
+ setState(classifyDiscussionReactionFromTargets(event, target, root))
})()
return () => {
diff --git a/src/lib/nostr-land-relay-eligibility.ts b/src/lib/nostr-land-relay-eligibility.ts
new file mode 100644
index 00000000..dd5658c7
--- /dev/null
+++ b/src/lib/nostr-land-relay-eligibility.ts
@@ -0,0 +1,45 @@
+import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
+import { normalizeAnyRelayUrl } from '@/lib/url'
+
+/**
+ * True when any URL’s host is `nostr.land` (e.g. `wss://nostr.land`, `wss://aggr.nostr.land`).
+ * Used to decide whether read fetches should prepend {@link AGGR_NOSTR_LAND_WSS} (except the primary home OP feed).
+ */
+export function relayUrlsMentionNostrLandDomain(urls: readonly string[]): boolean {
+ return urls.some((url) => {
+ const normalized = normalizeAnyRelayUrl(url) || String(url).trim()
+ if (!normalized) return false
+ try {
+ const parsed = new URL(normalized.replace(/^ws:\/\//i, 'http://').replace(/^wss:\/\//i, 'https://'))
+ return parsed.hostname.toLowerCase() === 'nostr.land'
+ } catch {
+ return false
+ }
+ })
+}
+
+let viewerStackMentionsNostrLand = false
+
+/**
+ * Synced from the logged-in viewer’s relay stack (favorites, relay sets, NIP-65, cache, HTTP lists).
+ * Service-layer reads use {@link getViewerRelayStackNostrLandAggrEligible} when building REQ targets.
+ */
+export function syncViewerRelayStackNostrLandAggrEligible(urls: readonly string[]): boolean {
+ viewerStackMentionsNostrLand = relayUrlsMentionNostrLandDomain(urls)
+ return viewerStackMentionsNostrLand
+}
+
+export function getViewerRelayStackNostrLandAggrEligible(): boolean {
+ return viewerStackMentionsNostrLand
+}
+
+/** Deduped prepend of aggr when the viewer opted into nostr.land relays (see sync…). */
+export function prependAggrNostrLandIfViewerEligible(relayUrls: readonly string[]): string[] {
+ if (!viewerStackMentionsNostrLand) return [...relayUrls]
+ const aggrNorm = (normalizeAnyRelayUrl(AGGR_NOSTR_LAND_WSS) || AGGR_NOSTR_LAND_WSS).toLowerCase()
+ const norm = (u: string) => (normalizeAnyRelayUrl(u) || u.trim()).toLowerCase()
+ if (relayUrls.some((u) => norm(u) === aggrNorm)) {
+ return [...relayUrls]
+ }
+ return [AGGR_NOSTR_LAND_WSS, ...relayUrls]
+}
diff --git a/src/lib/relay-url-priority.test.ts b/src/lib/relay-url-priority.test.ts
index 9c9b70a4..9a2837ef 100644
--- a/src/lib/relay-url-priority.test.ts
+++ b/src/lib/relay-url-priority.test.ts
@@ -6,6 +6,7 @@ import {
} from '@/lib/relay-url-priority'
import { buildProfilePageReadRelayUrls, getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays'
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
+import { syncViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility'
describe('filterContextAuthorReadRelaysForPublish', () => {
it('drops loopback, LAN, and .onion; keeps public relays', () => {
@@ -46,7 +47,8 @@ describe('stripMailboxLocalUrlsForRemoteViewers', () => {
})
describe('nostr.land aggregator feed relay policy', () => {
- it('keeps aggr.nostr.land in capped read feed relay stacks', () => {
+ it('keeps aggr.nostr.land in capped read feed relay stacks when viewer uses nostr.land relays', () => {
+ syncViewerRelayStackNostrLandAggrEligible(['wss://nostr.land/'])
const out = buildPrioritizedReadRelayUrls({
userReadRelays: [
'wss://reader-a.example/',
@@ -60,6 +62,7 @@ describe('nostr.land aggregator feed relay policy', () => {
expect(out).toHaveLength(3)
expect(out[0]).toBe('wss://aggr.nostr.land/')
+ syncViewerRelayStackNostrLandAggrEligible([])
})
it('excludes aggr.nostr.land from the favorites feed relay list', () => {
@@ -74,6 +77,7 @@ describe('nostr.land aggregator feed relay policy', () => {
describe('buildProfilePageReadRelayUrls', () => {
it('includes viewed author write relays for remote profile timelines', () => {
+ syncViewerRelayStackNostrLandAggrEligible(['wss://nostr.land/'])
const out = buildProfilePageReadRelayUrls(
[],
[],
@@ -85,9 +89,11 @@ describe('buildProfilePageReadRelayUrls', () => {
)
expect(out).toContain('wss://author-outbox.example/')
+ syncViewerRelayStackNostrLandAggrEligible([])
})
it('prioritizes viewed author write relays ahead of long read lists', () => {
+ syncViewerRelayStackNostrLandAggrEligible(['wss://nostr.land/'])
const out = buildProfilePageReadRelayUrls(
[],
[],
@@ -100,5 +106,6 @@ describe('buildProfilePageReadRelayUrls', () => {
expect(out[0]).toBe('wss://aggr.nostr.land/')
expect(out[1]).toBe('wss://author-outbox.example/')
+ syncViewerRelayStackNostrLandAggrEligible([])
})
})
diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx
index 5421ce34..5ed00b12 100644
--- a/src/providers/FeedProvider.tsx
+++ b/src/providers/FeedProvider.tsx
@@ -3,7 +3,7 @@ import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata'
import { buildAllFavoritesFeedRelayUrls } from '@/lib/home-feed-relays'
import logger from '@/lib/logger'
-import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
+import { syncViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
@@ -22,40 +22,27 @@ function relayUrlListIdentity(urls: string[]): string {
.join('\n')
}
-function relayListMentionsNostrLand(urls: readonly string[]): boolean {
- return urls.some((url) => {
- const normalized = normalizeAnyRelayUrl(url) || url.trim()
- if (!normalized) return false
- try {
- const parsed = new URL(normalized.replace(/^ws:\/\//i, 'http://').replace(/^wss:\/\//i, 'https://'))
- return parsed.hostname.toLowerCase() === 'nostr.land'
- } catch {
- return false
- }
- })
-}
-
function buildHomeReplyFeedRelayUrls(
primaryRelayUrls: string[],
inboxRelayUrls: string[],
cacheRelayUrls: string[],
httpRelayUrls: string[],
- includeNostrLandAggr: boolean,
blockedRelays: string[]
): string[] {
- return feedRelayPolicyUrls([
- { source: 'favorites', urls: primaryRelayUrls },
- { source: 'viewer-read', urls: inboxRelayUrls },
- { source: 'cache', urls: cacheRelayUrls },
- { source: 'http-index', urls: httpRelayUrls },
- ...(includeNostrLandAggr ? [{ source: 'read-only', urls: [AGGR_NOSTR_LAND_WSS] }] : [])
- ], {
- operation: 'read',
- blockedRelays,
- nostrLandAggr: 'never',
- applySocialKindBlockedFilter: false,
- allowThirdPartyLocalRelays: true
- })
+ return feedRelayPolicyUrls(
+ [
+ { source: 'favorites', urls: primaryRelayUrls },
+ { source: 'viewer-read', urls: inboxRelayUrls },
+ { source: 'cache', urls: cacheRelayUrls },
+ { source: 'http-index', urls: httpRelayUrls }
+ ],
+ {
+ operation: 'read',
+ blockedRelays,
+ applySocialKindBlockedFilter: false,
+ allowThirdPartyLocalRelays: true
+ }
+ )
}
export function FeedProvider({ children }: { children: ReactNode }) {
@@ -105,7 +92,6 @@ export function FeedProvider({ children }: { children: ReactNode }) {
[],
[],
[],
- false,
[]
)
)
@@ -120,21 +106,25 @@ export function FeedProvider({ children }: { children: ReactNode }) {
[]
)
- const lastHomeFeedUrlLogRef = useRef({ primary: '', reply: '' })
- const updateFeedRelayUrls = useCallback(() => {
- const primaryRelays = buildAllFavoritesFeedRelayUrls(favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls)
- const aggrEligibleRelayUrls = [
+ const viewerNostrLandAggrEligible = useMemo(() => {
+ const urls = [
...favoriteFeedRelayUrls,
...replyExtraRelayLayers.inboxRelayUrls,
...replyExtraRelayLayers.outboxRelayUrls,
- ...replyExtraRelayLayers.cacheRelayUrls
+ ...replyExtraRelayLayers.cacheRelayUrls,
+ ...replyExtraRelayLayers.httpRelayUrls
]
+ return syncViewerRelayStackNostrLandAggrEligible(urls)
+ }, [favoriteFeedRelayUrls, replyExtraRelayLayers])
+
+ const lastHomeFeedUrlLogRef = useRef({ primary: '', reply: '' })
+ const updateFeedRelayUrls = useCallback(() => {
+ const primaryRelays = buildAllFavoritesFeedRelayUrls(favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls)
const replyRelays = buildHomeReplyFeedRelayUrls(
primaryRelays,
replyExtraRelayLayers.inboxRelayUrls,
replyExtraRelayLayers.cacheRelayUrls,
replyExtraRelayLayers.httpRelayUrls,
- relayListMentionsNostrLand(aggrEligibleRelayUrls),
blockedRelays
)
const primaryId = relayUrlListIdentity(primaryRelays)
@@ -149,7 +139,14 @@ export function FeedProvider({ children }: { children: ReactNode }) {
}
setUrlStateIfChanged(setRelayUrls, primaryRelays)
setUrlStateIfChanged(setReplyRelayUrls, replyRelays)
- }, [favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls, replyExtraRelayLayers, setUrlStateIfChanged])
+ }, [
+ favoriteFeedRelayUrls,
+ blockedRelays,
+ primaryExtraRelayUrls,
+ replyExtraRelayLayers,
+ setUrlStateIfChanged,
+ viewerNostrLandAggrEligible
+ ])
const favoriteRelaysIdentity = useMemo(
() =>
diff --git a/src/services/client-events.service.ts b/src/services/client-events.service.ts
index 0a7642e0..e2381582 100644
--- a/src/services/client-events.service.ts
+++ b/src/services/client-events.service.ts
@@ -648,6 +648,16 @@ export class EventService {
return e
}
+ /**
+ * Session LRU only — for UI that must classify before async fetch (e.g. notification reactions).
+ * Does not query IndexedDB or relays; {@link fetchEvent} remains authoritative when missing.
+ */
+ peekHexIdNoteFromSessionCache(hexId: string): NEvent | undefined {
+ const id = hexId.trim().toLowerCase()
+ if (!/^[0-9a-f]{64}$/.test(id)) return undefined
+ return this.getSessionEventIfAllowed(id, true)
+ }
+
/**
* Pubkeys whose session-cached kind 0 matches a name / display_name / nip-05 substring (for search without IDB).
*/
diff --git a/src/services/client-replaceable-events.service.ts b/src/services/client-replaceable-events.service.ts
index c68625f8..15e96bb6 100644
--- a/src/services/client-replaceable-events.service.ts
+++ b/src/services/client-replaceable-events.service.ts
@@ -23,6 +23,7 @@ import type { QueryService } from './client-query.service'
import logger from '@/lib/logger'
import client from './client.service'
import { buildComprehensiveRelayList, buildExploreProfileAndUserRelayList } from '@/lib/relay-list-builder'
+import { prependAggrNostrLandIfViewerEligible } from '@/lib/nostr-land-relay-eligibility'
import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter'
export class ReplaceableEventService {
@@ -613,6 +614,7 @@ export class ReplaceableEventService {
} else {
relayUrls = [...FAST_READ_RELAY_URLS]
}
+ relayUrls = prependAggrNostrLandIfViewerEligible(relayUrls)
// Contacts + NIP-65 need the same patience as pins/payment: 100ms EOSE loses the race on slow relays
// and multi-author batches must not use replaceableRace (first EVENT may not be the latest per author).
const isSlowReplaceableBatch =
@@ -868,6 +870,8 @@ export class ReplaceableEventService {
throw new Error('Invalid id')
}
+ /** Used only when relay steps miss — UI should already show this from {@link useFetchProfile} IDB/session first. */
+ let sessionFallback: NEvent | undefined
if (!_skipCache) {
const sessionEv = client.eventService.getSessionMetadataForPubkey(pubkey)
if (sessionEv && !shouldDropEventOnIngest(sessionEv)) {
@@ -878,7 +882,7 @@ export class ReplaceableEventService {
await this.indexProfile(sessionEv)
void indexedDb.putReplaceableEvent(sessionEv).catch(() => {})
ReplaceableEventService.clearProfileFetchMiss(pubkey)
- return sessionEv
+ sessionFallback = sessionEv
}
}
@@ -886,7 +890,7 @@ export class ReplaceableEventService {
const relayHints = relays.length > 0 ? [...relays] : []
if (!_skipCache && relayHints.length === 0 && ReplaceableEventService.isProfileFetchMissCached(pubkey)) {
- return undefined
+ return sessionFallback
}
// CRITICAL: Always use relay hints from bech32 addresses (nprofile, naddr, nevent) when available
@@ -939,14 +943,16 @@ export class ReplaceableEventService {
]
: []
- const expandedRelays = [
- ...new Set([
- ...relayHints,
- ...authorRelays,
- ...PROFILE_FETCH_RELAY_URLS,
- ...FAST_READ_RELAY_URLS
- ])
- ]
+ const expandedRelays = prependAggrNostrLandIfViewerEligible(
+ Array.from(
+ new Set([
+ ...relayHints,
+ ...authorRelays,
+ ...PROFILE_FETCH_RELAY_URLS,
+ ...FAST_READ_RELAY_URLS
+ ])
+ )
+ )
const profileFromExpanded = await this.fetchReplaceableEvent(
pubkey,
@@ -976,8 +982,9 @@ export class ReplaceableEventService {
})
if (comprehensiveRelays.length > 0) {
+ const relaysForQuery = prependAggrNostrLandIfViewerEligible(comprehensiveRelays)
const events = await this.queryService.query(
- comprehensiveRelays,
+ relaysForQuery,
{
authors: [pubkey],
kinds: [kinds.Metadata]
@@ -1007,10 +1014,10 @@ export class ReplaceableEventService {
ReplaceableEventService.releaseProfileFallbackNetworkSlot()
}
- if (!_skipCache && relayHints.length === 0) {
+ if (!_skipCache && relayHints.length === 0 && !sessionFallback) {
ReplaceableEventService.rememberProfileFetchMiss(pubkey)
}
- return undefined
+ return sessionFallback
}
/**