()
- for (const event of clientFilteredEvents) {
+ const labelEvents = clientFilteredEvents.slice(0, Math.min(showCount + 24, clientFilteredEvents.length))
+ for (const event of labelEvents) {
const labels: string[] = []
for (const req of reqs) {
if (!eventMatchesSubRequestFilter(event, req.filter as Filter)) continue
@@ -4554,7 +4580,7 @@ const NoteList = forwardRef(
}
}
return map
- }, [clientFilteredEvents, subRequestsKey, feedReasonLabelsTick])
+ }, [clientFilteredEvents, subRequestsKey, feedReasonLabelsTick, showCount])
const list = (
@@ -4585,6 +4611,7 @@ const NoteList = forwardRef(
bottomNoteLabel={eventReasonLabelMap.get(event.id)}
deferAuthorAvatar
seenOnAllowlist={homeFeedActiveSeenOnAllowlist}
+ showPaymentAttestationAction={showPaymentAttestationAction}
/>
))
)}
diff --git a/src/components/Sidebar/SidebarCalendarWeekWidget.tsx b/src/components/Sidebar/SidebarCalendarWeekWidget.tsx
index a67972c9..a5b0ed7a 100644
--- a/src/components/Sidebar/SidebarCalendarWeekWidget.tsx
+++ b/src/components/Sidebar/SidebarCalendarWeekWidget.tsx
@@ -28,7 +28,7 @@ import { CalendarEventCoverImage } from '@/components/CalendarEventCoverImage'
import { Button } from '@/components/ui/button'
/** Global calendar REQ: relays often cap; larger limit reduces “missing” older-published rows for this week. */
-const FETCH_LIMIT = 1200
+const FETCH_LIMIT = 400
/** Supplementary `authors` REQ: community calls (e.g. Edufeed) may not appear in the global slice. */
const FOLLOWING_CALENDAR_AUTHORS_CAP = 200
const FOLLOWING_CALENDAR_AUTHORS_CHUNK = 80
@@ -37,7 +37,7 @@ const FOLLOWING_CALENDAR_CHUNK_LIMIT = 350
const LIST_MAX_HEIGHT_PX = 240
const SIDEBAR_CALENDAR_MAX_RELAYS = 24
/** Merge session cache so events already loaded in feeds (but missed by this REQ) still appear. */
-const SESSION_CALENDAR_MERGE_CAP = 5000
+const SESSION_CALENDAR_MERGE_CAP = 1200
export default function SidebarCalendarWeekWidget() {
const { t } = useTranslation()
diff --git a/src/constants.ts b/src/constants.ts
index a440e136..58a4cc58 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -945,7 +945,7 @@ export const PROFILE_FEED_KINDS = SUPPORTED_KINDS.filter(
)
/** REQ `limit` for profile page timelines (single feed; narrow with kind filter or 🔍 search). */
-export const PROFILE_TIMELINE_REQ_LIMIT = 500
+export const PROFILE_TIMELINE_REQ_LIMIT = 200
/** Long-form, wiki, and publication index events for the profile "Articles and Publications" tab. */
export const PROFILE_PUBLICATIONS_TAB_KINDS: readonly number[] = [
diff --git a/src/hooks/usePaymentAttestationStatus.tsx b/src/hooks/usePaymentAttestationStatus.tsx
index 9de3a8f8..c3ee212c 100644
--- a/src/hooks/usePaymentAttestationStatus.tsx
+++ b/src/hooks/usePaymentAttestationStatus.tsx
@@ -1,11 +1,15 @@
import { ExtendedKind } from '@/constants'
import {
- findPaymentAttestationForTarget,
getPaymentAttestationTargetId,
getSuperchatPaymentRecipientPubkey
} from '@/lib/superchat'
+import {
+ loadPaymentAttestationLocal,
+ peekCachedPaymentAttestation,
+ refreshPaymentAttestationFromRelays,
+ rememberPaymentAttestationFromPublish
+} from '@/lib/payment-attestation-cache'
import client from '@/services/client.service'
-import indexedDb from '@/services/indexed-db.service'
import { Event as NostrEvent } from 'nostr-tools'
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'
@@ -18,19 +22,7 @@ function attestationFilter(recipientPubkey: string, targetEventId: string) {
}
}
-function resolveAttestationMatch(
- attestations: NostrEvent[],
- targetEventId: string,
- recipientPubkey: string
-): NostrEvent | undefined {
- return findPaymentAttestationForTarget(attestations, targetEventId, recipientPubkey)
-}
-
export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined) {
- const [attested, setAttested] = useState(false)
- const [attestationEvent, setAttestationEvent] = useState(null)
- const [checking, setChecking] = useState(false)
-
const recipientPubkey = targetEvent ? getSuperchatPaymentRecipientPubkey(targetEvent) : null
const targetId = targetEvent?.id?.toLowerCase()
@@ -42,6 +34,18 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined)
[targetEvent?.id, recipientPubkey]
)
+ const cached = useMemo(
+ () =>
+ targetEvent?.id && recipientPubkey
+ ? peekCachedPaymentAttestation(targetEvent.id, recipientPubkey)
+ : undefined,
+ [targetEvent?.id, recipientPubkey, targetId]
+ )
+
+ const [attested, setAttested] = useState(Boolean(cached))
+ const [attestationEvent, setAttestationEvent] = useState(cached ?? null)
+ const [checking, setChecking] = useState(false)
+
const applyMatch = useCallback((match: NostrEvent | undefined) => {
if (!match) return
setAttestationEvent(match)
@@ -55,19 +59,24 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined)
if (attestation.pubkey.toLowerCase() !== recipientPubkey.toLowerCase()) return
const attestedId = getPaymentAttestationTargetId(attestation)
if (attestedId?.toLowerCase() !== targetEvent.id.toLowerCase()) return
+ rememberPaymentAttestationFromPublish(attestation)
applyMatch(attestation)
},
[applyMatch, recipientPubkey, targetEvent?.id]
)
useLayoutEffect(() => {
- setAttested(false)
- setAttestationEvent(null)
- if (!targetEvent?.id || !recipientPubkey || !filter) return
-
- const sessionHits = client.eventService.getSessionEventsMatchingFilters([filter], 5)
- applyMatch(resolveAttestationMatch(sessionHits, targetEvent.id, recipientPubkey))
- }, [applyMatch, filter, recipientPubkey, targetEvent?.id])
+ if (!targetEvent?.id || !recipientPubkey) {
+ setAttested(false)
+ setAttestationEvent(null)
+ return
+ }
+ const hit = peekCachedPaymentAttestation(targetEvent.id, recipientPubkey)
+ if (hit) {
+ setAttestationEvent(hit)
+ setAttested(true)
+ }
+ }, [recipientPubkey, targetEvent?.id])
useEffect(() => {
if (!targetEvent?.id || !recipientPubkey || !filter) return
@@ -77,20 +86,18 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined)
void (async () => {
try {
- const [idbAttestations, localFeedAttestations, relayAttestations] = await Promise.all([
- indexedDb.getPaymentAttestationsForTargetEvent(targetEvent.id, 20),
- client.getLocalFeedEvents([{ urls: [], filter }], { maxMatches: 5 }),
- client.fetchEvents([], filter, {
- cache: true,
- eoseTimeout: 4000,
- globalTimeout: 10_000
- })
- ])
-
+ const local = await loadPaymentAttestationLocal(targetEvent.id, recipientPubkey, filter)
if (cancelled) return
-
- const merged = [...idbAttestations, ...localFeedAttestations, ...relayAttestations]
- applyMatch(resolveAttestationMatch(merged, targetEvent.id, recipientPubkey))
+ if (local) {
+ applyMatch(local)
+ return
+ }
+ const relay = await refreshPaymentAttestationFromRelays(
+ targetEvent.id,
+ recipientPubkey,
+ filter
+ )
+ if (!cancelled) applyMatch(relay)
} catch {
/* optional */
} finally {
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 6c6047d7..0d352b3e 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -1368,7 +1368,7 @@ export default {
"Submit Relay": "Submit Relay",
Homepage: "Homepage",
"Proof of Work (difficulty {{minPow}})": "Proof of Work (difficulty {{minPow}})",
- "POW: difficulty {{difficulty}}": "POW: difficulty {{difficulty}}",
+ "POW {{difficulty}}": "POW {{difficulty}}",
"via {{client}}": "via {{client}}",
"Auto-load media": "Auto-load media",
Always: "Always",
diff --git a/src/lib/payment-attestation-cache.ts b/src/lib/payment-attestation-cache.ts
new file mode 100644
index 00000000..6614c7e2
--- /dev/null
+++ b/src/lib/payment-attestation-cache.ts
@@ -0,0 +1,95 @@
+import { ExtendedKind } from '@/constants'
+import { findPaymentAttestationForTarget } from '@/lib/superchat'
+import client from '@/services/client.service'
+import indexedDb from '@/services/indexed-db.service'
+import type { Event as NostrEvent, Filter } from 'nostr-tools'
+
+const attestationByTargetKey = new Map()
+const relayFetchByTargetKey = new Map>()
+
+export function paymentAttestationCacheKey(targetEventId: string, recipientPubkey: string): string {
+ return `${targetEventId.trim().toLowerCase()}:${recipientPubkey.trim().toLowerCase()}`
+}
+
+export function peekCachedPaymentAttestation(
+ targetEventId: string,
+ recipientPubkey: string
+): NostrEvent | undefined {
+ return attestationByTargetKey.get(paymentAttestationCacheKey(targetEventId, recipientPubkey))
+}
+
+export function rememberPaymentAttestation(
+ targetEventId: string,
+ recipientPubkey: string,
+ attestation: NostrEvent
+): void {
+ attestationByTargetKey.set(
+ paymentAttestationCacheKey(targetEventId, recipientPubkey),
+ attestation
+ )
+}
+
+export function resolvePaymentAttestationFromEvents(
+ events: NostrEvent[],
+ targetEventId: string,
+ recipientPubkey: string
+): NostrEvent | undefined {
+ const match = findPaymentAttestationForTarget(events, targetEventId, recipientPubkey)
+ if (match) {
+ rememberPaymentAttestation(targetEventId, recipientPubkey, match)
+ }
+ return match
+}
+
+export async function loadPaymentAttestationLocal(
+ targetEventId: string,
+ recipientPubkey: string,
+ filter: Filter
+): Promise {
+ const cached = peekCachedPaymentAttestation(targetEventId, recipientPubkey)
+ if (cached) return cached
+
+ const sessionHits = client.eventService.getSessionEventsMatchingFilters([filter], 5)
+ const fromSession = resolvePaymentAttestationFromEvents(sessionHits, targetEventId, recipientPubkey)
+ if (fromSession) return fromSession
+
+ const idbAttestations = await indexedDb.getPaymentAttestationsForTargetEvent(targetEventId, 20)
+ return resolvePaymentAttestationFromEvents(idbAttestations, targetEventId, recipientPubkey)
+}
+
+/** One coalesced relay refresh per payment target (shared by all visible superchat rows). */
+export async function refreshPaymentAttestationFromRelays(
+ targetEventId: string,
+ recipientPubkey: string,
+ filter: Filter
+): Promise {
+ const cached = peekCachedPaymentAttestation(targetEventId, recipientPubkey)
+ if (cached) return cached
+
+ const key = paymentAttestationCacheKey(targetEventId, recipientPubkey)
+ let inflight = relayFetchByTargetKey.get(key)
+ if (!inflight) {
+ inflight = client
+ .fetchEvents([], filter, {
+ cache: true,
+ eoseTimeout: 2500,
+ globalTimeout: 6000
+ })
+ .finally(() => {
+ if (relayFetchByTargetKey.get(key) === inflight) {
+ relayFetchByTargetKey.delete(key)
+ }
+ })
+ relayFetchByTargetKey.set(key, inflight)
+ }
+
+ const relayAttestations = await inflight
+ return resolvePaymentAttestationFromEvents(relayAttestations, targetEventId, recipientPubkey)
+}
+
+export function rememberPaymentAttestationFromPublish(attestation: NostrEvent): void {
+ if (attestation.kind !== ExtendedKind.PAYMENT_ATTESTATION) return
+ const targetId = attestation.tags.find(([name]) => name === 'e' || name === 'E')?.[1]?.trim().toLowerCase()
+ if (!targetId || !/^[0-9a-f]{64}$/.test(targetId)) return
+ rememberPaymentAttestation(targetId, attestation.pubkey, attestation)
+}
diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx
index 2d1a6866..02ce0da0 100644
--- a/src/pages/primary/SpellsPage/index.tsx
+++ b/src/pages/primary/SpellsPage/index.tsx
@@ -1108,6 +1108,7 @@ const SpellsPage = forwardRef(function SpellsPage(
hideUntrustedNotes={
selectedFauxSpell === 'notifications' ? hideUntrustedNotifications : false
}
+ showPaymentAttestationAction={selectedFauxSpell === 'notifications'}
/>
>
diff --git a/src/providers/LiveActivitiesProvider.tsx b/src/providers/LiveActivitiesProvider.tsx
index beb87aa0..fbcb6f6a 100644
--- a/src/providers/LiveActivitiesProvider.tsx
+++ b/src/providers/LiveActivitiesProvider.tsx
@@ -86,8 +86,8 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode
try {
const events = await client.fetchEvents(
urls,
- { kinds: [...LIVE_ACTIVITY_KINDS], limit: 500 },
- { eoseTimeout: 6000, globalTimeout: 14_000 }
+ { kinds: [...LIVE_ACTIVITY_KINDS], limit: 120 },
+ { eoseTimeout: 5000, globalTimeout: 10_000 }
)
const parentByAddress = await resolveParentSpacesForLiveActivities(events, urls, (u, f, o) =>
client.fetchEvents(u, f, o)
diff --git a/src/services/client-query.service.ts b/src/services/client-query.service.ts
index f247a5c6..b1cf68e4 100644
--- a/src/services/client-query.service.ts
+++ b/src/services/client-query.service.ts
@@ -281,6 +281,27 @@ export class QueryService {
* feed / prefetch / replaceable fetches yield to search and publish.
*/
private backgroundInterruptController = new AbortController()
+ /** Coalesce identical read-only REQs (no per-event callback) for a few seconds. */
+ private queryInFlightByKey = new Map>()
+
+ private buildReadQueryDedupKey(
+ relayUrls: readonly string[],
+ filters: readonly Filter[],
+ opts?: { globalTimeout?: number; eoseTimeout?: number }
+ ): string {
+ const relays = relayUrls
+ .map((u) => normalizeUrl(u) || u.trim())
+ .filter(Boolean)
+ .sort()
+ .join('|')
+ const filterKey = JSON.stringify(
+ filters.map((filter) => {
+ const entries = Object.entries(filter).sort(([a], [b]) => a.localeCompare(b))
+ return Object.fromEntries(entries)
+ })
+ )
+ return `${relays}::${filterKey}::${opts?.globalTimeout ?? 0}::${opts?.eoseTimeout ?? 0}`
+ }
/**
* Best-effort: abort in-flight {@link query} calls that did not pass `foreground: true`, then reset the token so
@@ -493,7 +514,19 @@ export class QueryService {
const foreground = options?.foreground === true
- return await new Promise((resolve) => {
+ const dedupKey =
+ !onevent && !foreground && !immediateReturn && !options?.signal?.aborted
+ ? this.buildReadQueryDedupKey([...wsQueryUrls, ...httpRelayBases], sanitizedFilters, {
+ globalTimeout,
+ eoseTimeout
+ })
+ : null
+ if (dedupKey) {
+ const inflight = this.queryInFlightByKey.get(dedupKey)
+ if (inflight) return inflight
+ }
+
+ const resultPromise = new Promise((resolve) => {
const events: NEvent[] = []
const cancelAbortRegistrations: Array<() => void> = []
const abortHttp = new AbortController()
@@ -767,6 +800,17 @@ export class QueryService {
globalTimeoutId = setTimeout(() => resolveWithEvents(), globalTimeout)
})
+
+ if (dedupKey) {
+ this.queryInFlightByKey.set(dedupKey, resultPromise)
+ void resultPromise.finally(() => {
+ if (this.queryInFlightByKey.get(dedupKey) === resultPromise) {
+ this.queryInFlightByKey.delete(dedupKey)
+ }
+ })
+ }
+
+ return resultPromise
}
/**