diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index 2b1aeee6..dcf0052f 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -1,4 +1,3 @@ -import { Skeleton } from '@/components/ui/skeleton' import { ExtendedKind, isNip71StyleVideoKind } from '@/constants' import { notificationReactionSummaryKey, @@ -371,9 +370,7 @@ export default function ContentPreview({ if (isNip25ReactionKind(event.kind)) { return withKindRow(
- {reactionDisplay.status === 'pending' ? ( - - ) : reactionDisplay.status === 'vote_up' ? ( + {reactionDisplay.status === 'vote_up' ? ( {DISCUSSION_UPVOTE_DISPLAY} 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 } /**