Browse Source

reveal reactions on the notes

imwald
Silberengel 22 hours ago
parent
commit
ef0298a3d0
  1. 6
      src/components/NoteInteractions/index.tsx
  2. 11
      src/components/NoteStats/LikeButton.tsx
  3. 2
      src/components/NoteStats/RepostButton.tsx
  4. 11
      src/components/NoteStats/index.tsx
  5. 5
      src/components/ReplyNote/index.tsx
  6. 55
      src/components/ReplyNoteList/index.tsx
  7. 4
      src/components/RssUrlThreadStatsBar/index.tsx
  8. 15
      src/pages/secondary/NotePage/index.tsx
  9. 18
      src/pages/secondary/RssArticlePage/index.tsx
  10. 134
      src/services/note-stats.service.ts

6
src/components/NoteInteractions/index.tsx

@ -11,12 +11,15 @@ import ReplySort, { ReplySortOption } from './ReplySort' @@ -11,12 +11,15 @@ import ReplySort, { ReplySortOption } from './ReplySort'
export default function NoteInteractions({
pageIndex,
event,
showQuotes: showQuotesProp
showQuotes: showQuotesProp,
statsForeground = false
}: {
pageIndex?: number
event: Event
/** When set, overrides the default (quotes hidden for discussions only). */
showQuotes?: boolean
/** Reply row stats use the same priority lane as the open note (`foregroundStats` on `NoteStats`). */
statsForeground?: boolean
}) {
const { t } = useTranslation()
const [replySort, setReplySort] = useState<ReplySortOption>('oldest')
@ -53,6 +56,7 @@ export default function NoteInteractions({ @@ -53,6 +56,7 @@ export default function NoteInteractions({
event={event}
sort={replySort}
showQuotes={showQuotes}
statsForeground={statsForeground}
/>
</>
)

11
src/components/NoteStats/LikeButton.tsx

@ -81,6 +81,9 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; @@ -81,6 +81,9 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
return { myLastEmoji: myLike?.emoji, likeCount: likes?.length, upVoteCount, downVoteCount }
}, [noteStats, pubkey, hideUntrustedInteractions, showDiscussionVotes])
/** Same idea as {@link ReplyButton}: merged likes (thread fetch / publish) can exist before snapshot sets `updatedAt`. */
const showLikeCount = !hideCount && (statsLoaded || (likeCount ?? 0) > 0)
const like = async (emoji: string | TEmoji) => {
checkLogin(async () => {
if (liking || !pubkey) return
@ -90,7 +93,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; @@ -90,7 +93,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
try {
if (!noteStats?.updatedAt) {
await noteStatsService.fetchNoteStats(event, pubkey, statsRelays)
await noteStatsService.fetchNoteStats(event, pubkey, statsRelays, { foreground: true })
}
const emojiString = typeof emoji === 'string' ? emoji : emoji.shortcode
@ -235,7 +238,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; @@ -235,7 +238,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
) : myLastEmoji ? (
<>
<Emoji emoji={inQuietMode ? '+' : myLastEmoji} classNames={{ img: EMOJI_IMG_INLINE_CLASS }} />
{!hideCount && statsLoaded && (
{showLikeCount && (
<div className="text-sm tabular-nums">
{(likeCount ?? 0) >= 100 ? '99+' : String(likeCount ?? 0)}
</div>
@ -244,7 +247,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; @@ -244,7 +247,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
) : (
<>
<SmilePlus />
{!hideCount && statsLoaded && (
{showLikeCount && (
<div className="text-sm tabular-nums">
{(likeCount ?? 0) >= 100 ? '99+' : String(likeCount ?? 0)}
</div>
@ -282,7 +285,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; @@ -282,7 +285,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
<span className="text-base leading-none" aria-hidden>
{arrow}
</span>
{!hideCount && noteStats?.updatedAt != null && (
{!hideCount && (noteStats?.updatedAt != null || count > 0) && (
<div className="text-sm tabular-nums">
{count >= 100 ? '99+' : count}
</div>

2
src/components/NoteStats/RepostButton.tsx

@ -57,7 +57,7 @@ export default function RepostButton({ event, hideCount = false }: { event: Even @@ -57,7 +57,7 @@ export default function RepostButton({ event, hideCount = false }: { event: Even
const hasReposted = noteStats?.repostPubkeySet?.has(pubkey)
if (hasReposted) return
if (!noteStats?.updatedAt) {
await noteStatsService.fetchNoteStats(event, pubkey, statsRelays)
await noteStatsService.fetchNoteStats(event, pubkey, statsRelays, { foreground: true })
// Note: fetchNoteStats doesn't return the stats, it updates them asynchronously
// The updated stats will be available through the useNoteStatsById hook
}

11
src/components/NoteStats/index.tsx

@ -23,7 +23,8 @@ export default function NoteStats({ @@ -23,7 +23,8 @@ export default function NoteStats({
className,
classNames,
fetchIfNotExisting = false,
displayTopZapsAndLikes = false
displayTopZapsAndLikes = false,
foregroundStats = false
}: {
event: Event
className?: string
@ -32,6 +33,8 @@ export default function NoteStats({ @@ -32,6 +33,8 @@ export default function NoteStats({
}
fetchIfNotExisting?: boolean
displayTopZapsAndLikes?: boolean
/** Jump ahead of spell-feed backlog so counts resolve on the open note / article. */
foregroundStats?: boolean
}) {
const { isSmallScreen } = useScreenSize()
const { pubkey } = useNostr()
@ -64,10 +67,12 @@ export default function NoteStats({ @@ -64,10 +67,12 @@ export default function NoteStats({
hintRelayCount: statsRelays.length
})
setLoading(true)
noteStatsService.fetchNoteStats(event, pubkey, statsRelays).finally(() => setLoading(false))
noteStatsService
.fetchNoteStats(event, pubkey, statsRelays, { foreground: foregroundStats })
.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.
}, [event.id, event.kind, event.created_at, event.sig, fetchIfNotExisting, pubkey, statsRelaysKey])
}, [event.id, event.kind, event.created_at, event.sig, fetchIfNotExisting, foregroundStats, pubkey, statsRelaysKey])
if (isSmallScreen) {
return (

5
src/components/ReplyNote/index.tsx

@ -44,7 +44,8 @@ export default function ReplyNote({ @@ -44,7 +44,8 @@ export default function ReplyNote({
onClickParent = () => {},
onClickReply,
highlight = false,
duplicateWebPreviewCleanedUrlHints
duplicateWebPreviewCleanedUrlHints,
foregroundStats = false
}: {
event: Event
parentEventId?: string
@ -52,6 +53,7 @@ export default function ReplyNote({ @@ -52,6 +53,7 @@ export default function ReplyNote({
onClickReply?: (event: Event) => void
highlight?: boolean
duplicateWebPreviewCleanedUrlHints?: string[]
foregroundStats?: boolean
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
@ -208,6 +210,7 @@ export default function ReplyNote({ @@ -208,6 +210,7 @@ export default function ReplyNote({
event={event}
displayTopZapsAndLikes={event.kind !== kinds.Zap}
fetchIfNotExisting
foregroundStats={foregroundStats}
/>
)}
</div>

55
src/components/ReplyNoteList/index.tsx

@ -232,6 +232,38 @@ function replyIdPresentInRepliesMap( @@ -232,6 +232,38 @@ function replyIdPresentInRepliesMap(
return false
}
/** NIP-25 reaction: any `e` / `E` tag value equals this hex id (lowercased). */
function noteReactionEtagEqualsHex(ev: NEvent, hexLower: string): boolean {
const h = hexLower.trim().toLowerCase()
if (!/^[0-9a-f]{64}$/i.test(h)) return false
for (const t of ev.tags) {
if ((t[0] === 'e' || t[0] === 'E') && typeof t[1] === 'string' && t[1].toLowerCase() === h) return true
}
return false
}
/**
* Thread REQ historically omitted kind 7; {@link replyMatchesThreadForList} also drops reactions from the reply list.
* Reactions still need to merge into {@link noteStatsService} for the root so the note header matches notifications.
*/
function mergeFetchedKind7ReactionsIntoRootNoteStats(all: NEvent[], rootInfo: TRootInfo) {
if (rootInfo.type === 'E') {
const rootHex = rootInfo.id.trim().toLowerCase()
const hits = all.filter((ev) => ev.kind === kinds.Reaction && noteReactionEtagEqualsHex(ev, rootHex))
if (hits.length > 0) {
noteStatsService.updateNoteStatsByEvents(hits, undefined, { interactionTargetNoteId: rootInfo.id })
}
} else if (rootInfo.type === 'A') {
const idHex = rootInfo.eventId?.trim().toLowerCase()
if (idHex && /^[0-9a-f]{64}$/i.test(idHex)) {
const hits = all.filter((ev) => ev.kind === kinds.Reaction && noteReactionEtagEqualsHex(ev, idHex))
if (hits.length > 0) {
noteStatsService.updateNoteStatsByEvents(hits, undefined, { interactionTargetNoteId: rootInfo.eventId })
}
}
}
}
function replyMatchesThreadForList(
evt: NEvent,
opEvent: NEvent,
@ -292,7 +324,8 @@ function ReplyNoteList({ @@ -292,7 +324,8 @@ function ReplyNoteList({
event,
sort = 'oldest',
showQuotes = true,
duplicateWebPreviewCleanedUrlHints
duplicateWebPreviewCleanedUrlHints,
statsForeground = false
}: {
index?: number
event: NEvent
@ -301,6 +334,8 @@ function ReplyNoteList({ @@ -301,6 +334,8 @@ function ReplyNoteList({
showQuotes?: boolean
/** Suppress WebPreview for these URLs in replies (e.g. article URL already shown as OP). */
duplicateWebPreviewCleanedUrlHints?: string[]
/** Passed through to reply row `NoteStats` on note & article pages. */
statsForeground?: boolean
}) {
const { t } = useTranslation()
const { navigateToNote } = useSmartNoteNavigation()
@ -1042,7 +1077,8 @@ function ReplyNoteList({ @@ -1042,7 +1077,8 @@ function ReplyNoteList({
const filters: Filter[] = []
if (rootInfo.type === 'E') {
// Fetch all reply types for event-based replies
// Fetch all reply types for event-based replies (keep ≤4 kinds per filter — some relays
// NOTICE "too many kinds N" and drop the whole REQ if kind 7 is bundled with four others).
filters.push({
'#e': [rootInfo.id],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, kinds.Zap],
@ -1054,6 +1090,11 @@ function ReplyNoteList({ @@ -1054,6 +1090,11 @@ function ReplyNoteList({
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, kinds.Zap],
limit: LIMIT
})
filters.push({
'#e': [rootInfo.id],
kinds: [kinds.Reaction],
limit: LIMIT
})
// Kind-1 notes that quote via #q without e-tags (still part of this thread)
filters.push({
'#q': [rootInfo.id],
@ -1082,6 +1123,13 @@ function ReplyNoteList({ @@ -1082,6 +1123,13 @@ function ReplyNoteList({
limit: LIMIT
}
)
if (/^[0-9a-f]{64}$/i.test(rootInfo.eventId)) {
filters.push({
'#e': [rootInfo.eventId],
kinds: [kinds.Reaction],
limit: LIMIT
})
}
const qVals = Array.from(
new Set(
[rootInfo.eventId, rootInfo.id]
@ -1126,6 +1174,8 @@ function ReplyNoteList({ @@ -1126,6 +1174,8 @@ function ReplyNoteList({
if (fetchGeneration !== replyFetchGenRef.current) return
mergeFetchedKind7ReactionsIntoRootNoteStats(allReplies, rootInfo)
// Filter and add replies (URL threads include kind 9802 highlights of this page)
const regularReplies = allReplies.filter((evt) => {
const match = replyMatchesThreadForList(evt, event, rootInfo, isDiscussionRoot)
@ -1441,6 +1491,7 @@ function ReplyNoteList({ @@ -1441,6 +1491,7 @@ function ReplyNoteList({
event={reply}
parentEventId={event.id !== parentEventHexId ? parentEventId : undefined}
duplicateWebPreviewCleanedUrlHints={replyDuplicateWebPreviewHints}
foregroundStats={statsForeground}
onClickParent={() => {
if (!parentEventHexId) return
if (replies.every((r) => r.id !== parentEventHexId)) {

4
src/components/RssUrlThreadStatsBar/index.tsx

@ -25,7 +25,9 @@ export default function RssUrlThreadStatsBar({ @@ -25,7 +25,9 @@ export default function RssUrlThreadStatsBar({
useEffect(() => {
setLoading(true)
noteStatsService.fetchNoteStats(event, pubkey, statsRelays).finally(() => setLoading(false))
noteStatsService
.fetchNoteStats(event, pubkey, statsRelays, { foreground: true })
.finally(() => setLoading(false))
}, [event.id, event.kind, event.created_at, event.sig, pubkey, statsRelaysKey])
const fmt = (n: number) => (n >= 100 ? '99+' : String(n))

15
src/pages/secondary/NotePage/index.tsx

@ -515,11 +515,22 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }: @@ -515,11 +515,22 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
}
/>
<NoteBoostBadges event={finalEvent} className="mt-2" />
<NoteStats className="mt-3" event={finalEvent} fetchIfNotExisting displayTopZapsAndLikes />
<NoteStats
className="mt-3"
event={finalEvent}
fetchIfNotExisting
displayTopZapsAndLikes
foregroundStats
/>
</div>
<Separator className="mt-4" />
<div className="px-4 pb-4 w-full">
<NoteInteractions key={`note-interactions-${finalEvent.id}`} pageIndex={index} event={finalEvent} />
<NoteInteractions
key={`note-interactions-${finalEvent.id}`}
pageIndex={index}
event={finalEvent}
statsForeground
/>
</div>
</SecondaryPageLayout>
)

18
src/pages/secondary/RssArticlePage/index.tsx

@ -299,7 +299,13 @@ const RssArticlePage = forwardRef( @@ -299,7 +299,13 @@ const RssArticlePage = forwardRef(
) : null}
{showNostrThread && syntheticRoot ? (
<div className="px-0 w-full">
<NoteStats className="mt-2" event={syntheticRoot} fetchIfNotExisting displayTopZapsAndLikes />
<NoteStats
className="mt-2"
event={syntheticRoot}
fetchIfNotExisting
displayTopZapsAndLikes
foregroundStats
/>
</div>
) : null}
{showNostrThread ? <Separator /> : null}
@ -310,6 +316,7 @@ const RssArticlePage = forwardRef( @@ -310,6 +316,7 @@ const RssArticlePage = forwardRef(
pageIndex={index}
event={syntheticRoot}
showQuotes={false}
statsForeground
/>
) : null}
</div>
@ -388,7 +395,13 @@ const RssArticlePage = forwardRef( @@ -388,7 +395,13 @@ const RssArticlePage = forwardRef(
</div>
{showNostrThread && syntheticRoot ? (
<div className="px-4 w-full">
<NoteStats className="mt-3" event={syntheticRoot} fetchIfNotExisting displayTopZapsAndLikes />
<NoteStats
className="mt-3"
event={syntheticRoot}
fetchIfNotExisting
displayTopZapsAndLikes
foregroundStats
/>
</div>
) : null}
{showNostrThread ? <Separator className="mt-4" /> : null}
@ -399,6 +412,7 @@ const RssArticlePage = forwardRef( @@ -399,6 +412,7 @@ const RssArticlePage = forwardRef(
pageIndex={index}
event={syntheticRoot}
showQuotes={false}
statsForeground
/>
) : null}
</div>

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

@ -63,6 +63,10 @@ class NoteStatsService { @@ -63,6 +63,10 @@ class NoteStatsService {
// Batch processing
private pendingEvents = new Set<string>()
/** Open note / explicit UI: drained before {@link pendingEvents} so detail pages are not stuck behind feed cards. */
private pendingForeground = new Set<string>()
/** If a foreground fetch hit {@link processingCache}, re-queue here so the follow-up run uses the priority lane. */
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>()
/** Merged favorite URLs requested while this note was already in {@link processingCache}. */
@ -112,31 +116,87 @@ class NoteStatsService { @@ -112,31 +116,87 @@ class NoteStatsService {
}, this.BATCH_DELAY)
}
async fetchNoteStats(event: Event, _pubkey?: string | null, favoriteRelays?: string[] | null) {
private statsPendingSize() {
return this.pendingForeground.size + this.pendingEvents.size
}
/** Up to {@link MAX_BATCH_SIZE} ids, foreground queue first (same insertion order within each set). */
private takeNextStatsSlice(): string[] {
const out: string[] = []
for (const id of this.pendingForeground) {
if (out.length >= this.MAX_BATCH_SIZE) break
this.pendingForeground.delete(id)
out.push(id)
}
for (const id of this.pendingEvents) {
if (out.length >= this.MAX_BATCH_SIZE) break
this.pendingEvents.delete(id)
out.push(id)
}
return out
}
/** Coalesce scroll bursts; flush immediately when backlog is large or a foreground note was queued. */
private maybeFlushStatsBatch(foreground: boolean) {
if (this.processBatchRunning) {
return
}
const backlogLarge = this.pendingEvents.size >= this.MAX_BATCH_SIZE
if (backlogLarge || foreground) {
if (this.batchTimeout) {
clearTimeout(this.batchTimeout)
this.batchTimeout = null
}
void this.processBatch()
} else {
this.armStatsBatchTimer()
}
}
/**
* Queue relay-backed stats for `event`. Foreground (`opts.foreground`) is for the focused note page /
* article detail so counts are not starved behind spell-feed cards (large pending backlog).
*/
async fetchNoteStats(
event: Event,
_pubkey?: string | null,
favoriteRelays?: string[] | null,
opts?: { foreground?: boolean }
) {
const eventId = this.statsKey(event.id)
const idShort = `${eventId.slice(0, 12)}`
const foreground = opts?.foreground === true
if (this.pendingEvents.has(eventId)) {
this.mergeFavoriteRelaysIntoPending(eventId, favoriteRelays)
const rememberRoot = () => {
if (event.kind === ExtendedKind.RSS_THREAD_ROOT) {
this.pendingSyntheticRootById.set(eventId, event)
} else {
this.pendingStatsRootEventById.set(eventId, event)
}
}
if (this.pendingEvents.has(eventId) || this.pendingForeground.has(eventId)) {
this.mergeFavoriteRelaysIntoPending(eventId, favoriteRelays)
rememberRoot()
if (foreground) {
this.pendingEvents.delete(eventId)
this.pendingForeground.add(eventId)
}
logger.debug('[NoteStats] fetchNoteStats: merged into existing pending batch', {
eventId: idShort,
kind: event.kind,
pendingSize: this.pendingEvents.size
pendingForeground: this.pendingForeground.size,
pendingBackground: this.pendingEvents.size
})
this.maybeFlushStatsBatch(foreground)
return
}
if (this.processingCache.has(eventId)) {
this.mergeFavoriteRelaysIntoDeferred(eventId, favoriteRelays)
if (event.kind === ExtendedKind.RSS_THREAD_ROOT) {
this.pendingSyntheticRootById.set(eventId, event)
} else {
this.pendingStatsRootEventById.set(eventId, event)
rememberRoot()
if (foreground) {
this.deferredRequeueForeground.add(eventId)
}
logger.debug('[NoteStats] fetchNoteStats: deferred (already processing same id)', {
eventId: idShort,
@ -146,42 +206,48 @@ class NoteStatsService { @@ -146,42 +206,48 @@ class NoteStatsService {
}
this.pendingFetchFavoriteRelays.set(eventId, favoriteRelays ?? null)
this.pendingEvents.add(eventId)
if (event.kind === ExtendedKind.RSS_THREAD_ROOT) {
this.pendingSyntheticRootById.set(eventId, event)
if (foreground) {
this.pendingForeground.add(eventId)
} else {
this.pendingStatsRootEventById.set(eventId, event)
this.pendingEvents.add(eventId)
}
rememberRoot()
logger.debug('[NoteStats] fetchNoteStats: queued new id', {
eventId: idShort,
kind: event.kind,
pendingSize: this.pendingEvents.size,
immediateBatch: this.pendingEvents.size >= this.MAX_BATCH_SIZE
foreground,
pendingForeground: this.pendingForeground.size,
pendingBackground: this.pendingEvents.size,
immediateBatch: this.statsPendingSize() >= this.MAX_BATCH_SIZE
})
this.armStatsBatchTimer()
if (this.pendingEvents.size >= this.MAX_BATCH_SIZE && !this.processBatchRunning) {
if (this.batchTimeout) {
clearTimeout(this.batchTimeout)
this.batchTimeout = null
this.maybeFlushStatsBatch(foreground)
}
private scheduleStatsBatchContinuation() {
if (this.pendingForeground.size === 0 && this.pendingEvents.size === 0) return
queueMicrotask(() => {
void this.processBatch()
}
})
}
private async processBatch() {
if (this.processBatchRunning) {
logger.debug('[NoteStats] processBatch: skipped (already running)', {
pendingSize: this.pendingEvents.size
pendingForeground: this.pendingForeground.size,
pendingBackground: this.pendingEvents.size
})
return
}
if (this.pendingEvents.size === 0) {
if (this.pendingForeground.size === 0 && this.pendingEvents.size === 0) {
return
}
logger.info('[NoteStats] processBatch: running', { pendingSize: this.pendingEvents.size })
logger.info('[NoteStats] processBatch: running', {
pendingForeground: this.pendingForeground.size,
pendingBackground: this.pendingEvents.size
})
this.processBatchRunning = true
if (this.batchTimeout) {
clearTimeout(this.batchTimeout)
@ -189,26 +255,22 @@ class NoteStatsService { @@ -189,26 +255,22 @@ class NoteStatsService {
}
try {
while (this.pendingEvents.size > 0) {
const eventsToProcess = Array.from(this.pendingEvents).slice(0, this.MAX_BATCH_SIZE)
for (const id of eventsToProcess) {
this.pendingEvents.delete(id)
}
const eventsToProcess = this.takeNextStatsSlice()
logger.info('[NoteStats] processBatch slice', {
count: eventsToProcess.length,
ids: eventsToProcess.map((id) => `${id.slice(0, 12)}`),
remainingPending: this.pendingEvents.size,
remainingForeground: this.pendingForeground.size,
remainingBackground: this.pendingEvents.size,
concurrency: this.STATS_SLICE_CONCURRENCY
})
for (let i = 0; i < eventsToProcess.length; i += this.STATS_SLICE_CONCURRENCY) {
const chunk = eventsToProcess.slice(i, i + this.STATS_SLICE_CONCURRENCY)
await Promise.all(chunk.map((eventId) => this.processSingleEvent(eventId)))
}
}
} finally {
this.processBatchRunning = false
if (this.pendingEvents.size > 0) {
this.armStatsBatchTimer()
if (this.pendingForeground.size > 0 || this.pendingEvents.size > 0) {
this.scheduleStatsBatchContinuation()
}
}
}
@ -327,17 +389,23 @@ class NoteStatsService { @@ -327,17 +389,23 @@ class NoteStatsService {
if (this.inFlightDeferredFavoriteRelays.has(eventId)) {
const deferred = this.inFlightDeferredFavoriteRelays.get(eventId)!
this.inFlightDeferredFavoriteRelays.delete(eventId)
const requeueForeground = this.deferredRequeueForeground.has(eventId)
this.deferredRequeueForeground.delete(eventId)
if (deferred.length > 0) {
if (this.pendingEvents.has(eventId)) {
if (this.pendingEvents.has(eventId) || this.pendingForeground.has(eventId)) {
this.mergeFavoriteRelaysIntoPending(eventId, deferred)
} else {
this.pendingFetchFavoriteRelays.set(eventId, deferred)
if (requeueForeground) {
this.pendingForeground.add(eventId)
} else {
this.pendingEvents.add(eventId)
}
}
}
}
}
}
/**
* Build relay list for note stats: SEARCHABLE + FAST_READ + optional user favorites + seen relays +

Loading…
Cancel
Save