Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
51bfb0e48d
  1. 10
      src/components/NormalFeed/index.tsx
  2. 5
      src/components/NoteCard/MainNoteCard.tsx
  3. 5
      src/components/NoteCard/RepostNoteCard.tsx
  4. 7
      src/components/NoteCard/index.tsx
  5. 36
      src/components/NoteList/index.tsx
  6. 18
      src/components/NoteStats/SeenOnButton.tsx
  7. 21
      src/components/NoteStats/index.tsx
  8. 34
      src/lib/relay-allowlist.test.ts
  9. 58
      src/lib/relay-allowlist.ts
  10. 2
      src/lib/relay-url-priority.ts
  11. 2
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  12. 46
      src/services/note-stats.service.ts

10
src/components/NormalFeed/index.tsx

@ -124,6 +124,10 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -124,6 +124,10 @@ const NormalFeed = forwardRef<TNoteListRef, {
* Single-relay explore: only events from that relays live REQ (no session/IDB prime, no prefetch to other relays).
*/
relayAuthoritativeFeedOnly?: boolean
/** Home favorites Notes tab: favorites + trending relays for stats / “Seen on”. */
homeFeedSeenOnAllowlistOp?: string[]
/** Home favorites Replies / Gallery: adds NIP-65, cache, and HTTP index read relays. */
homeFeedSeenOnAllowlistReplies?: string[]
}>(function NormalFeed(
{
subRequests,
@ -160,7 +164,9 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -160,7 +164,9 @@ const NormalFeed = forwardRef<TNoteListRef, {
oneShotMergedCap,
timelinePublicReadFallback = false,
alexandriaEmptyUrl = null,
relayAuthoritativeFeedOnly = false
relayAuthoritativeFeedOnly = false,
homeFeedSeenOnAllowlistOp,
homeFeedSeenOnAllowlistReplies
},
ref
) {
@ -400,6 +406,8 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -400,6 +406,8 @@ const NormalFeed = forwardRef<TNoteListRef, {
followingFeedDeltaSubRequests={followingFeedDeltaSubRequests}
feedTimelineScopeKey={feedTimelineScopeKey}
homeFeedListMode={isMainFeed ? listMode : undefined}
homeFeedSeenOnAllowlistOp={homeFeedSeenOnAllowlistOp}
homeFeedSeenOnAllowlistReplies={homeFeedSeenOnAllowlistReplies}
gridLayout={listMode === 'media'}
revealBatchSize={listMode === 'media' && isMainFeed ? 96 : undefined}
useFilterAsIs={listMode === 'media' ? true : useFilterAsIs}

5
src/components/NoteCard/MainNoteCard.tsx

@ -25,7 +25,8 @@ export default function MainNoteCard({ @@ -25,7 +25,8 @@ export default function MainNoteCard({
showFull = false,
fetchNoteStatsIfMissing = true,
deferAuthorAvatar = false,
searchListPreview = false
searchListPreview = false,
seenOnAllowlist
}: {
event: Event
className?: string
@ -44,6 +45,7 @@ export default function MainNoteCard({ @@ -44,6 +45,7 @@ export default function MainNoteCard({
deferAuthorAvatar?: boolean
/** Compact row: no stats bar, no separator, no boost badges (e.g. merged NIP-50 search). */
searchListPreview?: boolean
seenOnAllowlist?: readonly string[]
}) {
const { t } = useTranslation()
const { navigateToNote } = useSmartNoteNavigationOptional()
@ -130,6 +132,7 @@ export default function MainNoteCard({ @@ -130,6 +132,7 @@ export default function MainNoteCard({
className={embedded ? 'mt-2 px-2 sm:px-3' : `mt-3 ${notePadX}`}
event={event}
fetchIfNotExisting={fetchNoteStatsIfMissing}
seenOnAllowlist={seenOnAllowlist}
/>
) : null}
{!embedded && bottomNoteLabel ? (

5
src/components/NoteCard/RepostNoteCard.tsx

@ -16,7 +16,8 @@ export default function RepostNoteCard({ @@ -16,7 +16,8 @@ export default function RepostNoteCard({
filterMutedNotes = true,
pinned = false,
bottomNoteLabel,
deferAuthorAvatar = true
deferAuthorAvatar = true,
seenOnAllowlist
}: {
event: Event
className?: string
@ -24,6 +25,7 @@ export default function RepostNoteCard({ @@ -24,6 +25,7 @@ export default function RepostNoteCard({
pinned?: boolean
bottomNoteLabel?: string
deferAuthorAvatar?: boolean
seenOnAllowlist?: readonly string[]
}) {
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
@ -102,6 +104,7 @@ export default function RepostNoteCard({ @@ -102,6 +104,7 @@ export default function RepostNoteCard({
pinned={pinned}
bottomNoteLabel={bottomNoteLabel}
deferAuthorAvatar={deferAuthorAvatar}
seenOnAllowlist={seenOnAllowlist}
/>
)
}

7
src/components/NoteCard/index.tsx

@ -18,7 +18,8 @@ const NoteCard = memo(function NoteCard({ @@ -18,7 +18,8 @@ const NoteCard = memo(function NoteCard({
bottomNoteLabel,
fetchNoteStatsIfMissing = true,
deferAuthorAvatar = true,
searchListPreview = false
searchListPreview = false,
seenOnAllowlist
}: {
event: Event
className?: string
@ -31,6 +32,7 @@ const NoteCard = memo(function NoteCard({ @@ -31,6 +32,7 @@ const NoteCard = memo(function NoteCard({
fetchNoteStatsIfMissing?: boolean
deferAuthorAvatar?: boolean
searchListPreview?: boolean
seenOnAllowlist?: readonly string[]
}) {
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
@ -57,6 +59,7 @@ const NoteCard = memo(function NoteCard({ @@ -57,6 +59,7 @@ const NoteCard = memo(function NoteCard({
pinned={pinned}
bottomNoteLabel={bottomNoteLabel}
deferAuthorAvatar={deferAuthorAvatar}
seenOnAllowlist={seenOnAllowlist}
/>
)
}
@ -70,6 +73,7 @@ const NoteCard = memo(function NoteCard({ @@ -70,6 +73,7 @@ const NoteCard = memo(function NoteCard({
fetchNoteStatsIfMissing={fetchNoteStatsIfMissing}
deferAuthorAvatar={deferAuthorAvatar}
searchListPreview={searchListPreview}
seenOnAllowlist={seenOnAllowlist}
/>
)
}, (prevProps, nextProps) => {
@ -83,6 +87,7 @@ const NoteCard = memo(function NoteCard({ @@ -83,6 +87,7 @@ const NoteCard = memo(function NoteCard({
prevProps.hideParentNotePreview === nextProps.hideParentNotePreview &&
prevProps.bottomNoteLabel === nextProps.bottomNoteLabel &&
prevProps.fetchNoteStatsIfMissing === nextProps.fetchNoteStatsIfMissing &&
prevProps.seenOnAllowlist === nextProps.seenOnAllowlist &&
prevProps.deferAuthorAvatar === nextProps.deferAuthorAvatar &&
prevProps.searchListPreview === nextProps.searchListPreview
)

36
src/components/NoteList/index.tsx

@ -23,6 +23,7 @@ import { @@ -23,6 +23,7 @@ import {
isSpellSubRequestsSameFiltersDifferentRelays
} from '@/lib/spell-feed-request-identity'
import logger from '@/lib/logger'
import { eventSeenOnMatchesAllowlist } from '@/lib/relay-allowlist'
import { isLocalNetworkUrl, normalizeUrl } from '@/lib/url'
import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter'
import { collectLocalEventsForTextSearch } from '@/lib/local-nip50-search-merge'
@ -671,6 +672,10 @@ const NoteList = forwardRef( @@ -671,6 +672,10 @@ const NoteList = forwardRef(
* unrelated picker churn stale grid + refresh feeling broken.
*/
homeFeedListMode,
/** Home favorites: relays allowed for “Seen on” + stats on the Notes tab (favorites + trending). */
homeFeedSeenOnAllowlistOp,
/** Home favorites: wider stack for Replies / Gallery (adds NIP-65, cache, HTTP index). */
homeFeedSeenOnAllowlistReplies,
/** Spells page: bumps when user picks a feed; used with {@link onSpellFeedFirstPaint}. */
spellFeedInstrumentToken,
/** Spells page: fired once when the filtered list first has rows after a picker change. */
@ -788,6 +793,8 @@ const NoteList = forwardRef( @@ -788,6 +793,8 @@ const NoteList = forwardRef(
followingFeedDeltaSubRequests?: TFeedSubRequest[]
feedTimelineScopeKey?: string
homeFeedListMode?: TNoteListMode
homeFeedSeenOnAllowlistOp?: string[]
homeFeedSeenOnAllowlistReplies?: string[]
spellFeedInstrumentToken?: number
onSpellFeedFirstPaint?: (detail: { eventCount: number; firstEventId: string }) => void
timelineLoadingSafetyTimeoutMs?: number
@ -1041,6 +1048,19 @@ const NoteList = forwardRef( @@ -1041,6 +1048,19 @@ const NoteList = forwardRef(
const timelineSubscriptionKey = feedSubscriptionKey ?? subRequestsKey
const homeFeedActiveSeenOnAllowlist = useMemo(() => {
if (feedSubscriptionKey !== 'home-all-favorites') return undefined
if (homeFeedListMode === 'postsAndReplies' || homeFeedListMode === 'media') {
return homeFeedSeenOnAllowlistReplies?.length ? homeFeedSeenOnAllowlistReplies : undefined
}
return homeFeedSeenOnAllowlistOp?.length ? homeFeedSeenOnAllowlistOp : undefined
}, [
feedSubscriptionKey,
homeFeedListMode,
homeFeedSeenOnAllowlistOp,
homeFeedSeenOnAllowlistReplies
])
const prevSubRequestsKeyForTimelineRef = useRef<string | null>(null)
const feedTimelineScopePrevRef = useRef<string | undefined>(undefined)
/** Detect pull-to-refresh so preserve-mode feeds still clear; unrelated dep changes must not clear. */
@ -1241,6 +1261,17 @@ const NoteList = forwardRef( @@ -1241,6 +1261,17 @@ const NoteList = forwardRef(
if (extraShouldHideEvent?.(evt)) return true
if (
homeFeedActiveSeenOnAllowlist &&
homeFeedListMode === 'posts' &&
!eventSeenOnMatchesAllowlist(
client.getSeenEventRelayUrls(evt.id),
homeFeedActiveSeenOnAllowlist
)
) {
return true
}
return false
},
[
@ -1252,7 +1283,9 @@ const NoteList = forwardRef( @@ -1252,7 +1283,9 @@ const NoteList = forwardRef(
pinnedEventIds,
isEventDeleted,
zapReplyThreshold,
extraShouldHideEvent
extraShouldHideEvent,
homeFeedActiveSeenOnAllowlist,
homeFeedListMode
]
)
@ -4481,6 +4514,7 @@ const NoteList = forwardRef( @@ -4481,6 +4514,7 @@ const NoteList = forwardRef(
filterMutedNotes={filterMutedNotes}
bottomNoteLabel={eventReasonLabelMap.get(event.id)}
deferAuthorAvatar
seenOnAllowlist={homeFeedActiveSeenOnAllowlist}
/>
))
)}

18
src/components/NoteStats/SeenOnButton.tsx

@ -10,6 +10,7 @@ import { @@ -10,6 +10,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { toRelay } from '@/lib/link'
import { filterRelaysToUserAllowlist } from '@/lib/relay-allowlist'
import { simplifyUrl } from '@/lib/url'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
@ -19,7 +20,14 @@ import { useEffect, useState } from 'react' @@ -19,7 +20,14 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
export default function SeenOnButton({ event }: { event: Event }) {
export default function SeenOnButton({
event,
/** When set (home favorites feed), only list relays from the feed allowlist. */
allowedRelays
}: {
event: Event
allowedRelays?: readonly string[]
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { push } = useSecondaryPage()
@ -32,8 +40,10 @@ export default function SeenOnButton({ event }: { event: Event }) { @@ -32,8 +40,10 @@ export default function SeenOnButton({ event }: { event: Event }) {
const maxAttempts = 20
const apply = () => {
const seenOn = client.getSeenEventRelayUrls(event.id)
if (!cancelled) setRelays(seenOn)
return seenOn.length > 0
const visible =
allowedRelays?.length ? filterRelaysToUserAllowlist(seenOn, allowedRelays) : seenOn
if (!cancelled) setRelays(visible)
return visible.length > 0
}
if (apply()) return
const id = setInterval(() => {
@ -45,7 +55,7 @@ export default function SeenOnButton({ event }: { event: Event }) { @@ -45,7 +55,7 @@ export default function SeenOnButton({ event }: { event: Event }) {
cancelled = true
clearInterval(id)
}
}, [event.id])
}, [event.id, allowedRelays])
const trigger = (
<button

21
src/components/NoteStats/index.tsx

@ -25,7 +25,9 @@ export default function NoteStats({ @@ -25,7 +25,9 @@ export default function NoteStats({
fetchIfNotExisting = false,
foregroundStats = false,
deferFetchUntilNearViewport,
useIconOnlyLikeTrigger = false
useIconOnlyLikeTrigger = false,
/** Home feed: stats + “Seen on” only use these relays (favorites + trending, or reply widen stack). */
seenOnAllowlist
}: {
event: Event
className?: string
@ -44,6 +46,7 @@ export default function NoteStats({ @@ -44,6 +46,7 @@ export default function NoteStats({
* Thread rows for kind-7 reactions: like control shows icon + total only (body already shows the reaction glyph).
*/
useIconOnlyLikeTrigger?: boolean
seenOnAllowlist?: readonly string[]
}) {
const { pubkey } = useNostr()
const noteStats = useNoteStatsById(event.id)
@ -61,7 +64,11 @@ export default function NoteStats({ @@ -61,7 +64,11 @@ export default function NoteStats({
/** Synthetic RSS article root: no boost/quote/zap bar entries that normal notes have. */
const isRssArticleRoot = event.kind === ExtendedKind.RSS_THREAD_ROOT
/** Match {@link RssUrlThreadStatsBar}: inbox/favorites/fast-read merge — plain hints miss many #i indexers. */
const statsRelays = isRssArticleRoot ? rssUrlThreadRelays : hintRelays
const statsRelays = isRssArticleRoot
? rssUrlThreadRelays
: seenOnAllowlist?.length
? seenOnAllowlist
: hintRelays
/** At most two background refetches per card: before vs after inbox/favorite hints hydrate. */
const statsRelayFetchTier = isRssArticleRoot ? relayMergeTier : hintRelays.length > 0 ? 1 : 0
const statsRelaysRef = useRef(statsRelays)
@ -76,7 +83,10 @@ export default function NoteStats({ @@ -76,7 +83,10 @@ export default function NoteStats({
if (shouldDeferStatsFetch && !isNearViewport) return
setLoading(true)
noteStatsService
.fetchNoteStats(event, pubkey, statsRelaysRef.current, { foreground: foregroundStats })
.fetchNoteStats(event, pubkey, statsRelaysRef.current, {
foreground: foregroundStats,
relayAllowlist: seenOnAllowlist?.length ? seenOnAllowlist : null
})
.finally(() => setLoading(false))
// Intentionally omit `event` object: parent feeds often pass new references each render;
// id/sig/kind/created_at identify the note for refetch boundaries.
@ -92,7 +102,8 @@ export default function NoteStats({ @@ -92,7 +102,8 @@ export default function NoteStats({
isNearViewport,
pubkey,
statsRelayFetchTier,
currentRelaysKey
currentRelaysKey,
seenOnAllowlist
])
const interactionButtons = (
@ -138,7 +149,7 @@ export default function NoteStats({ @@ -138,7 +149,7 @@ export default function NoteStats({
<div className="flex min-w-0 flex-wrap items-center">{interactionButtons}</div>
<div className="flex shrink-0 flex-wrap items-center">
{utilityButtons}
<SeenOnButton event={event} />
<SeenOnButton event={event} allowedRelays={seenOnAllowlist} />
</div>
</div>
</div>

34
src/lib/relay-allowlist.test.ts

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import {
eventSeenOnMatchesAllowlist,
filterRelaysToUserAllowlist,
isRelayInUserAllowlist
} from '@/lib/relay-allowlist'
describe('relay-allowlist', () => {
const allow = ['wss://theforest.nostr1.com/', 'wss://feeds.nostrarchives.com/notes/trending/reactions/today']
it('matches hostname across schemes', () => {
expect(isRelayInUserAllowlist('wss://theforest.nostr1.com/', allow)).toBe(true)
expect(isRelayInUserAllowlist('https://theforest.nostr1.com/', allow)).toBe(true)
expect(isRelayInUserAllowlist('wss://nostr.wine/', allow)).toBe(false)
})
it('filters relay lists to the allowlist', () => {
expect(
filterRelaysToUserAllowlist(
['wss://nostr.wine/', 'wss://theforest.nostr1.com/', 'wss://theforest.nostr1.com/'],
allow
)
).toEqual(['wss://theforest.nostr1.com/'])
})
it('eventSeenOnMatchesAllowlist allows empty seen-on and blocks unknown relays', () => {
expect(eventSeenOnMatchesAllowlist([], allow)).toBe(true)
expect(eventSeenOnMatchesAllowlist(['wss://theforest.nostr1.com/'], allow)).toBe(true)
expect(eventSeenOnMatchesAllowlist(['wss://nostr.wine/'], allow)).toBe(false)
expect(
eventSeenOnMatchesAllowlist(['wss://nostr.wine/', 'wss://theforest.nostr1.com/'], allow)
).toBe(true)
})
})

58
src/lib/relay-allowlist.ts

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
import { normalizeAnyRelayUrl } from '@/lib/url'
function relayHostname(url: string): string | null {
const normalized = normalizeAnyRelayUrl(url) || url.trim()
if (!normalized) return null
try {
return new URL(normalized).hostname.toLowerCase()
} catch {
return null
}
}
function relayMatchesEntry(url: string, entry: string): boolean {
const normalized = normalizeAnyRelayUrl(url) || url.trim()
if (!normalized) return false
const entryNorm = normalizeAnyRelayUrl(entry) || entry.trim()
if (!entryNorm) return false
if (entryNorm === normalized) return true
const host = relayHostname(normalized)
return Boolean(host && relayHostname(entryNorm) === host)
}
/** True when the relay matches an allowlist URL or shares its hostname (https vs wss). */
export function isRelayInUserAllowlist(url: string, allowlist?: readonly string[]): boolean {
if (!allowlist?.length) return false
return allowlist.some((entry) => relayMatchesEntry(url, entry))
}
export function filterRelaysToUserAllowlist(
urls: readonly string[],
allowlist?: readonly string[]
): string[] {
if (!allowlist?.length) return [...urls]
const seen = new Set<string>()
const out: string[] = []
for (const raw of urls) {
const n = normalizeAnyRelayUrl(raw) || raw.trim()
if (!n || !isRelayInUserAllowlist(n, allowlist)) continue
const k = n.toLowerCase()
if (seen.has(k)) continue
seen.add(k)
out.push(n)
}
return out
}
/**
* When the session has recorded delivery relays, require at least one on the allowlist.
* Empty seen-on (e.g. fresh live REQ row) is treated as allowed.
*/
export function eventSeenOnMatchesAllowlist(
seenRelayUrls: readonly string[],
allowlist: readonly string[]
): boolean {
if (!allowlist.length) return true
if (seenRelayUrls.length === 0) return true
return seenRelayUrls.some((u) => isRelayInUserAllowlist(u, allowlist))
}

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

@ -9,7 +9,7 @@ import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url @@ -9,7 +9,7 @@ import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url
export { MAX_REQ_RELAY_URLS }
export function dedupeNormalizeRelayUrlsOrdered(urls: string[]): string[] {
export function dedupeNormalizeRelayUrlsOrdered(urls: readonly string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const u of urls) {

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

@ -117,6 +117,8 @@ const RelaysFeed = forwardRef< @@ -117,6 +117,8 @@ const RelaysFeed = forwardRef<
widenMainGalleryRelays={false}
feedSubscriptionKey="home-all-favorites"
feedTimelineScopeKey="all-favorites"
homeFeedSeenOnAllowlistOp={relayUrls}
homeFeedSeenOnAllowlistReplies={replyRelayUrls}
showFeedClientFilter
hostPrimaryPageName="feed"
/>

46
src/services/note-stats.service.ts

@ -22,7 +22,10 @@ import { @@ -22,7 +22,10 @@ import {
} from '@/lib/rss-article'
import { eventReferencesThreadTarget, threadRootRefFromStatsRootEvent } from '@/lib/op-reference-tags'
import type { TThreadRootRef } from '@/lib/thread-reply-root-match'
import { filterRelaysToUserAllowlist, isRelayInUserAllowlist } from '@/lib/relay-allowlist'
import { prependAggrNostrLandIfViewerEligible } from '@/lib/nostr-land-relay-eligibility'
import { buildComprehensiveRelayList, relayHintsFromEventTags } from '@/lib/relay-list-builder'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import {
getEmojiInfosFromEmojiTags,
@ -86,6 +89,8 @@ class NoteStatsService { @@ -86,6 +89,8 @@ class NoteStatsService {
private deferredRequeueForeground = new Set<string>()
/** Favorite relays passed from the last fetchNoteStats call per note (used in processSingleEvent). */
private pendingFetchFavoriteRelays = new Map<string, string[] | null | undefined>()
/** Home feed: restrict stats REQs to favorites + trending (or reply widen stack) — no FAST_READ. */
private pendingFetchRelayAllowlist = new Map<string, readonly string[] | undefined>()
/** Merged favorite URLs requested while this note was already in {@link processingCache}. */
private inFlightDeferredFavoriteRelays = new Map<string, string[]>()
private batchTimeout: NodeJS.Timeout | null = null
@ -186,7 +191,7 @@ class NoteStatsService { @@ -186,7 +191,7 @@ class NoteStatsService {
event: Event,
_pubkey?: string | null,
favoriteRelays?: string[] | null,
opts?: { foreground?: boolean }
opts?: { foreground?: boolean; relayAllowlist?: readonly string[] | null }
) {
const eventId = this.statsKey(event.id)
const foreground = opts?.foreground === true
@ -233,6 +238,11 @@ class NoteStatsService { @@ -233,6 +238,11 @@ class NoteStatsService {
}
this.pendingFetchFavoriteRelays.set(eventId, favoriteRelays ?? null)
if (opts?.relayAllowlist?.length) {
this.pendingFetchRelayAllowlist.set(eventId, opts.relayAllowlist)
} else {
this.pendingFetchRelayAllowlist.delete(eventId)
}
if (foreground) {
this.pendingForeground.add(eventId)
} else {
@ -257,7 +267,7 @@ class NoteStatsService { @@ -257,7 +267,7 @@ class NoteStatsService {
_pubkey?: string | null,
opts?: { foreground?: boolean; threadRootHexId?: string }
): Promise<void> {
const urls = (relayUrls ?? []).filter(Boolean)
const urls = prependAggrNostrLandIfViewerEligible((relayUrls ?? []).filter(Boolean))
const hexReplies: Event[] = []
const replaceableReplies: Event[] = []
const oddIdReplies: Event[] = []
@ -468,6 +478,8 @@ class NoteStatsService { @@ -468,6 +478,8 @@ class NoteStatsService {
const favoriteRelays = this.pendingFetchFavoriteRelays.get(eventId)
this.pendingFetchFavoriteRelays.delete(eventId)
const relayAllowlist = this.pendingFetchRelayAllowlist.get(eventId)
this.pendingFetchRelayAllowlist.delete(eventId)
let publishedStatsSnapshot = false
const markStatsLoaded = (rawStatsKey: string) => {
@ -514,7 +526,11 @@ class NoteStatsService { @@ -514,7 +526,11 @@ class NoteStatsService {
}
}
const finalRelayUrls = await this.buildNoteStatsRelayList(resolvedEvent, favoriteRelays)
const finalRelayUrls = await this.buildNoteStatsRelayList(
resolvedEvent,
favoriteRelays,
relayAllowlist
)
const replaceableCoordinate = isReplaceableEvent(resolvedEvent.kind)
? getReplaceableCoordinateFromEvent(resolvedEvent)
@ -591,8 +607,17 @@ class NoteStatsService { @@ -591,8 +607,17 @@ class NoteStatsService {
}
}
/** Stats REQs: dedupe, then prepend {@link AGGR_NOSTR_LAND_WSS} when the viewer lists `wss://nostr.land`. */
private finalizeNoteStatsRelayUrls(urls: readonly string[]): string[] {
return prependAggrNostrLandIfViewerEligible(dedupeNormalizeRelayUrlsOrdered(urls))
}
/** {@link buildComprehensiveRelayList} for reactions/reposts/zaps on a note (thread hints, capped author NIP-65). */
private async buildNoteStatsRelayList(event: Event, favoriteRelays?: string[] | null): Promise<string[]> {
private async buildNoteStatsRelayList(
event: Event,
favoriteRelays?: string[] | null,
relayAllowlist?: readonly string[]
): Promise<string[]> {
const me = client.pubkey?.trim()
const relayHints = [
...relayHintsFromEventTags(event),
@ -601,6 +626,16 @@ class NoteStatsService { @@ -601,6 +626,16 @@ class NoteStatsService {
...(favoriteRelays ?? [])
]
if (relayAllowlist?.length) {
const onAllowlist = (u: string) => isRelayInUserAllowlist(u, relayAllowlist)
return this.finalizeNoteStatsRelayUrls(
filterRelaysToUserAllowlist(
[...relayAllowlist, ...relayHints.filter(onAllowlist)],
relayAllowlist
)
)
}
let useGlobal = true
if (me) {
try {
@ -618,7 +653,7 @@ class NoteStatsService { @@ -618,7 +653,7 @@ class NoteStatsService {
}
}
return buildComprehensiveRelayList({
const comprehensive = await buildComprehensiveRelayList({
authorPubkey: event.pubkey,
userPubkey: me,
relayHints,
@ -631,6 +666,7 @@ class NoteStatsService { @@ -631,6 +666,7 @@ class NoteStatsService {
includeLocalRelays: true,
includeViewerHttpIndexRelays: true
})
return this.finalizeNoteStatsRelayUrls(comprehensive)
}
/**

Loading…
Cancel
Save