Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
efedaf9296
  1. 29
      src/components/RssFeedList/index.tsx
  2. 22
      src/hooks/useFetchCalendarRsvps.tsx
  3. 58
      src/hooks/useFetchProfile.tsx
  4. 125
      src/hooks/useProfileTimeline.tsx
  5. 2
      src/i18n/locales/de.ts
  6. 2
      src/i18n/locales/en.ts
  7. 77
      src/lib/rss-web-feed.ts
  8. 76
      src/pages/primary/CalendarPrimaryPage.tsx
  9. 26
      src/pages/primary/SpellsPage/index.tsx
  10. 26
      src/pages/secondary/ProfileInteractionDiagramPage/index.tsx
  11. 43
      src/services/mention-event-search.service.ts

29
src/components/RssFeedList/index.tsx

@ -11,6 +11,7 @@ import { RssUnifiedScopeSection } from './RssUnifiedScopeSection'
import { canonicalizeRssArticleUrl, isClawstrDotComHttpUrl } from '@/lib/rss-article' import { canonicalizeRssArticleUrl, isClawstrDotComHttpUrl } from '@/lib/rss-article'
import { import {
addManualRssWebUrl, addManualRssWebUrl,
discoverRssWebArticleUrlsFromLocalCaches,
fetchDiscoveredWebUrlsFromRelays, fetchDiscoveredWebUrlsFromRelays,
loadManualRssWebUrls, loadManualRssWebUrls,
loadPromotedRssThreadUrls, loadPromotedRssThreadUrls,
@ -21,6 +22,7 @@ import {
isHttpArticleUrl, isHttpArticleUrl,
isRssWebUnifiedClutterUrl, isRssWebUnifiedClutterUrl,
mergeDiscoveredRssWebUrls, mergeDiscoveredRssWebUrls,
mergeManualRssWebUrlEntries,
rssWebRowHasRealFeedItems, rssWebRowHasRealFeedItems,
saveRssWebFeedScopePreference, saveRssWebFeedScopePreference,
saveRssWebHideUnifiedClutterPreference, saveRssWebHideUnifiedClutterPreference,
@ -669,14 +671,22 @@ export default function RssFeedList() {
let cancelled = false let cancelled = false
void (async () => { void (async () => {
try { try {
const discovered = await fetchDiscoveredWebUrlsFromRelays({ const local = await discoverRssWebArticleUrlsFromLocalCaches({
accountPubkey: pubkey,
favoriteRelays: favoriteRelays ?? [],
blockedRelays: blockedRelays ?? [],
excludeClutterUrls: hideUnifiedClutter excludeClutterUrls: hideUnifiedClutter
}) })
let discovered: ManualRssWebUrlEntry[] = []
try {
discovered = await fetchDiscoveredWebUrlsFromRelays({
accountPubkey: pubkey,
favoriteRelays: favoriteRelays ?? [],
blockedRelays: blockedRelays ?? [],
excludeClutterUrls: hideUnifiedClutter
})
} catch {
/* relay discovery is best-effort */
}
if (cancelled) return if (cancelled) return
setRelayDiscoveredUrls(discovered) setRelayDiscoveredUrls(mergeManualRssWebUrlEntries(local, discovered))
const didMerge = await mergeDiscoveredRssWebUrls(discovered) const didMerge = await mergeDiscoveredRssWebUrls(discovered)
if (didMerge && !cancelled) refreshManualWebUrls() if (didMerge && !cancelled) refreshManualWebUrls()
} catch { } catch {
@ -1114,6 +1124,15 @@ export default function RssFeedList() {
? t('No URL-only items yet') ? t('No URL-only items yet')
: t('No RSS feed items available')} : t('No RSS feed items available')}
</p> </p>
{feedScope === 'urls' &&
!searchQuery.trim() &&
selectedFeeds.includes('all') &&
timeFilter === 'all' &&
rssScopeRows.length > 0 ? (
<p className="mt-3 max-w-md text-center text-xs text-muted-foreground">
{t('RSS+Web url tab empty hint')}
</p>
) : null}
</div> </div>
) : feedScope === 'urls' ? ( ) : feedScope === 'urls' ? (
<> <>

22
src/hooks/useFetchCalendarRsvps.tsx

@ -73,18 +73,17 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
const userWrite = userWriteRelaysForQuery(relayList) const userWrite = userWriteRelaysForQuery(relayList)
void (async () => { void (async () => {
// Read order: IndexedDB first (offline + last session), then in-memory session, then relays.
let fromIdb: Event[] = []
try {
fromIdb = await indexedDb.getCalendarRsvpEventsByParentCoordinate(coordinate)
} catch {
fromIdb = []
}
if (cancelled) return
const fromSession = client.getSessionCalendarRsvpsForCalendarEvent(calendarEvent) const fromSession = client.getSessionCalendarRsvpsForCalendarEvent(calendarEvent)
const mergedLocal = mergeRsvpList([...fromIdb, ...fromSession]) setRsvps(mergeRsvpList(fromSession))
setRsvps(mergedLocal)
const idbP = indexedDb
.getCalendarRsvpEventsByParentCoordinate(coordinate)
.catch((): Event[] => [])
void idbP.then((rows) => {
if (cancelled) return
setRsvps(mergeRsvpList([...rows, ...fromSession]))
})
const baseUrls = new Set<string>([ const baseUrls = new Set<string>([
...FAST_READ_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url), ...FAST_READ_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url),
@ -139,6 +138,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
) )
if (cancelled) return if (cancelled) return
const fromRelay = events ?? [] const fromRelay = events ?? []
const fromIdb = await idbP
await Promise.allSettled( await Promise.allSettled(
fromRelay.map((ev) => indexedDb.putCalendarRsvpEventRow(ev).catch(() => undefined)) fromRelay.map((ev) => indexedDb.putCalendarRsvpEventRow(ev).catch(() => undefined))
) )

58
src/hooks/useFetchProfile.tsx

@ -12,6 +12,31 @@ import { kinds } from 'nostr-tools'
import { useEffect, useState, useRef, useCallback } from 'react' import { useEffect, useState, useRef, useCallback } from 'react'
import logger from '@/lib/logger' import logger from '@/lib/logger'
function tryHydrateProfileFromSessionOnly(pubkey: string, skipCache: boolean): TProfile | null {
if (skipCache) return null
const pk = pubkey.toLowerCase()
const sessionEv = eventService.getSessionMetadataForPubkey(pk)
if (sessionEv) {
return getProfileFromEvent(sessionEv)
}
return null
}
/** Single-flight IndexedDB kind-0 read (never blocks callers until they await the promise). */
function profileFromIdbPromise(pubkey: string, skipCache: boolean): Promise<TProfile | null> {
if (skipCache) return Promise.resolve(null)
const pk = pubkey.toLowerCase()
return indexedDb
.getReplaceableEvent(pk, kinds.Metadata)
.then((idbEv) => {
if (idbEv && !shouldDropEventOnIngest(idbEv)) {
return getProfileFromEvent(idbEv)
}
return null
})
.catch(() => null)
}
/** /**
* Session LRU + IndexedDB kind 0 without ReplaceableEventService / batched DataLoader. * Session LRU + IndexedDB kind 0 without ReplaceableEventService / batched DataLoader.
* Used when the hook's fetch race times out or the batch path is slow while disk/session already has metadata. * Used when the hook's fetch race times out or the batch path is slow while disk/session already has metadata.
@ -20,23 +45,9 @@ async function tryHydrateProfileFromLocalCaches(
pubkey: string, pubkey: string,
skipCache: boolean skipCache: boolean
): Promise<TProfile | null> { ): Promise<TProfile | null> {
if (skipCache) return null const fromSession = tryHydrateProfileFromSessionOnly(pubkey, skipCache)
const pk = pubkey.toLowerCase() if (fromSession) return fromSession
return profileFromIdbPromise(pubkey, skipCache)
const sessionEv = eventService.getSessionMetadataForPubkey(pk)
if (sessionEv) {
return getProfileFromEvent(sessionEv)
}
try {
const idbEv = await indexedDb.getReplaceableEvent(pk, kinds.Metadata)
if (idbEv && !shouldDropEventOnIngest(idbEv)) {
return getProfileFromEvent(idbEv)
}
} catch {
/* IDB not ready */
}
return null
} }
// CRITICAL: Global deduplication - shared across ALL hook instances // CRITICAL: Global deduplication - shared across ALL hook instances
@ -199,11 +210,12 @@ export function useFetchProfile(id?: string, skipCache = false) {
// Create a new fetch promise with timeout protection // Create a new fetch promise with timeout protection
const fetchPromise = (async (): Promise<TProfile | null> => { const fetchPromise = (async (): Promise<TProfile | null> => {
let idbEarlyP: Promise<TProfile | null> | null = null
try { try {
globalFetchingPubkeys.add(pubkey) globalFetchingPubkeys.add(pubkey)
const startTime = Date.now() const startTime = Date.now()
const quick = await tryHydrateProfileFromLocalCaches(pubkey, skipCache) const quick = tryHydrateProfileFromSessionOnly(pubkey, skipCache)
if (quick) { if (quick) {
logger.debug('[useFetchProfile] Profile from session/IndexedDB (fast path)', { logger.debug('[useFetchProfile] Profile from session/IndexedDB (fast path)', {
pubkey: pubkey.substring(0, 8), pubkey: pubkey.substring(0, 8),
@ -212,6 +224,9 @@ export function useFetchProfile(id?: string, skipCache = false) {
return quick return quick
} }
/** Disk read runs in parallel with `fetchProfileEvent` — never block network on IDB. */
idbEarlyP = profileFromIdbPromise(pubkey, skipCache)
// CRITICAL: Add timeout to prevent infinite hangs (must exceed batched metadata query globalTimeout) // CRITICAL: Add timeout to prevent infinite hangs (must exceed batched metadata query globalTimeout)
const timeoutPromise = new Promise<never>((_, reject) => { const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => { setTimeout(() => {
@ -263,7 +278,8 @@ export function useFetchProfile(id?: string, skipCache = false) {
fetchTime: `${fetchTime}ms` fetchTime: `${fetchTime}ms`
}) })
} }
const afterMiss = await tryHydrateProfileFromLocalCaches(pubkey, skipCache) const afterMiss =
(idbEarlyP != null ? await idbEarlyP : null) ?? tryHydrateProfileFromSessionOnly(pubkey, skipCache)
if (afterMiss) { if (afterMiss) {
logger.debug('[useFetchProfile] Profile from session/IndexedDB after network miss', { logger.debug('[useFetchProfile] Profile from session/IndexedDB after network miss', {
pubkey: pubkey.substring(0, 8), pubkey: pubkey.substring(0, 8),
@ -281,7 +297,9 @@ export function useFetchProfile(id?: string, skipCache = false) {
}) })
// Set cooldown period after timeout to prevent cascade of duplicate fetches // Set cooldown period after timeout to prevent cascade of duplicate fetches
globalFetchCooldowns.set(pubkey, Date.now() + 10000) // 10 second cooldown globalFetchCooldowns.set(pubkey, Date.now() + 10000) // 10 second cooldown
const fallback = await tryHydrateProfileFromLocalCaches(pubkey, skipCache) const fallback =
tryHydrateProfileFromSessionOnly(pubkey, skipCache) ??
(idbEarlyP != null ? await idbEarlyP : null)
if (fallback) { if (fallback) {
logger.debug('[useFetchProfile] Profile from session/IndexedDB after fetch timeout', { logger.debug('[useFetchProfile] Profile from session/IndexedDB after fetch timeout', {
pubkey: pubkey.substring(0, 8), pubkey: pubkey.substring(0, 8),

125
src/hooks/useProfileTimeline.tsx

@ -243,103 +243,72 @@ export function useProfileTimeline({
const socialKinds = kinds.some(isSocialKindBlockedKind) const socialKinds = kinds.some(isSocialKindBlockedKind)
const emptyAuthor = { read: [] as string[], write: [] as string[], httpRead: [] as string[], httpWrite: [] as string[] } const emptyAuthor = { read: [] as string[], write: [] as string[], httpRead: [] as string[], httpWrite: [] as string[] }
const idbDocKinds = kinds.filter((k) => isDocumentRelayKind(k)) const idbDocKinds = kinds.filter((k) => isDocumentRelayKind(k))
/**
* Author NIP-65 read/write relays must feed the **first** REQ for every profile tab. Favorites-only
* misses most peoples kind-1 notes; we previously only prefetched relays for document tabs.
*/
let prefetchedAuthorRelays: typeof emptyAuthor = emptyAuthor
if (idbDocKinds.length > 0) { let pkNorm: string | null = null
try {
pkNorm = normalizeHexPubkey(pubkey)
} catch {
pkNorm = null
}
let hadSessionHits = false
if (pkNorm) {
const pkForDisk = pkNorm
try { try {
const pkNorm = normalizeHexPubkey(pubkey) const sessionKindList = idbDocKinds.length > 0 ? idbDocKinds : kinds
const fromSession = eventService.listSessionEventsAuthoredBy(pkNorm, { const fromSession = eventService.listSessionEventsAuthoredBy(pkForDisk, {
kinds: idbDocKinds, kinds: sessionKindList,
limit limit
}) })
hadSessionHits = fromSession.length > 0
if (!cancelled) { if (!cancelled) {
for (const e of fromSession) { for (const e of fromSession) {
pool.set(e.id, e as Event) pool.set(e.id, e as Event)
} }
if (fromSession.length) flushPool() if (fromSession.length) flushPool()
} }
const [authorRl, fromPubStore, fromArchive] = await Promise.all([
client.fetchRelayList(pubkey).catch(() => ({
read: [] as string[],
write: [] as string[],
httpRead: [] as string[],
httpWrite: [] as string[]
})),
indexedDb.getCachedPublicationStoreEventsForProfileAuthor(pkNorm, idbDocKinds, limit),
indexedDb.scanEventArchiveByAuthorPubkey(pkNorm, {
kinds: idbDocKinds,
maxRowsScanned: 18_000,
maxMatches: limit
})
])
if (!cancelled) {
prefetchedAuthorRelays = authorRl
for (const e of fromPubStore) {
pool.set(e.id, e)
}
for (const e of fromArchive) {
pool.set(e.id, e)
}
const hadDisk = fromPubStore.length > 0 || fromArchive.length > 0
if (hadDisk) flushPool()
else if (!isCacheFresh && !mem?.events?.length && fromSession.length === 0) {
setIsLoading(true)
}
}
} catch { } catch {
if (!cancelled) { /* ignore malformed pubkeys */
prefetchedAuthorRelays = await client.fetchRelayList(pubkey).catch(() => emptyAuthor)
}
if (!cancelled && !isCacheFresh && !mem?.events?.length) {
setIsLoading(true)
}
} }
} else {
try { void (async () => {
const pkNorm = normalizeHexPubkey(pubkey) try {
const fromSession = eventService.listSessionEventsAuthoredBy(pkNorm, { kinds, limit }) const idbKindsForScan = idbDocKinds.length > 0 ? idbDocKinds : kinds
if (!cancelled) { const maxScan = idbDocKinds.length > 0 ? 18_000 : 16_000
for (const e of fromSession) { const pubStorePromise =
pool.set(e.id, e as Event) idbDocKinds.length > 0
} ? indexedDb.getCachedPublicationStoreEventsForProfileAuthor(pkForDisk, idbDocKinds, limit)
if (fromSession.length) flushPool() : Promise.resolve([] as Event[])
} const [fromPubStore, fromArchive] = await Promise.all([
const [authorRl, fromArchiveSocial] = await Promise.all([ pubStorePromise,
client.fetchRelayList(pubkey).catch(() => emptyAuthor), indexedDb.scanEventArchiveByAuthorPubkey(pkForDisk, {
indexedDb.scanEventArchiveByAuthorPubkey(pkNorm, { kinds: idbKindsForScan,
kinds, maxRowsScanned: maxScan,
maxRowsScanned: 16_000, maxMatches: limit
maxMatches: limit })
}) ])
]) if (cancelled) return
if (!cancelled) { for (const e of fromPubStore) pool.set(e.id, e)
prefetchedAuthorRelays = authorRl for (const e of fromArchive) pool.set(e.id, e)
for (const e of fromArchiveSocial) { const hadDisk = fromPubStore.length + fromArchive.length > 0
pool.set(e.id, e) if (hadDisk) flushPool()
} else if (!isCacheFresh && !mem?.events?.length && !hadSessionHits) {
if (fromArchiveSocial.length) flushPool()
else if (!isCacheFresh && !mem?.events?.length && fromSession.length === 0) {
setIsLoading(true) setIsLoading(true)
} }
} catch {
/* best-effort */
} }
} catch { })()
if (!cancelled) { } else if (!isCacheFresh && !mem?.events?.length) {
prefetchedAuthorRelays = await client.fetchRelayList(pubkey).catch(() => emptyAuthor) setIsLoading(true)
}
if (!cancelled && !isCacheFresh && !mem?.events?.length) {
setIsLoading(true)
}
}
} }
const authorRelayPromise = client.fetchRelayList(pubkey).catch(() => emptyAuthor)
const provisionalFeedUrls = buildProfilePageReadRelayUrls( const provisionalFeedUrls = buildProfilePageReadRelayUrls(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
prefetchedAuthorRelays, emptyAuthor,
socialKinds, socialKinds,
includeAuthorLocalRelays, includeAuthorLocalRelays,
kinds kinds
@ -429,7 +398,7 @@ export function useProfileTimeline({
})() })()
void (async () => { void (async () => {
const authorRl = prefetchedAuthorRelays const authorRl = await authorRelayPromise
if (cancelled) return if (cancelled) return
const fullFeedUrls = buildProfilePageReadRelayUrls( const fullFeedUrls = buildProfilePageReadRelayUrls(
favoriteRelays, favoriteRelays,

2
src/i18n/locales/de.ts

@ -1675,6 +1675,8 @@ export default {
URLs: "URLs", URLs: "URLs",
RSS: "RSS", RSS: "RSS",
"No URL-only items yet": "Noch keine reinen Artikel-URLs", "No URL-only items yet": "Noch keine reinen Artikel-URLs",
"RSS+Web url tab empty hint":
"Artikel aus deinen RSS-Feeds findest du unter dem Tab RSS. Hier erscheinen Artikel-URLs aus Nostr (Reaktionen, Kommentare, Lesezeichen auf Webseiten), wenn sie nicht schon vollständig als Feed-Einträge abgedeckt sind, sowie manuell hinzugefügte Links.",
"Respond to this RSS entry": "Auf diesen RSS-Eintrag reagieren", "Respond to this RSS entry": "Auf diesen RSS-Eintrag reagieren",
"RSS read-only thread hint": "Nostr-Antworten, Zaps und Markierungen sind hier ausgeblendet. Damit fügst du den Artikel der URL-Liste hinzu und reagierst dort.", "RSS read-only thread hint": "Nostr-Antworten, Zaps und Markierungen sind hier ausgeblendet. Damit fügst du den Artikel der URL-Liste hinzu und reagierst dort.",
"RSS feed item label": "RSS", "RSS feed item label": "RSS",

2
src/i18n/locales/en.ts

@ -1695,6 +1695,8 @@ export default {
URLs: "URLs", URLs: "URLs",
RSS: "RSS", RSS: "RSS",
"No URL-only items yet": "No URL-only items yet", "No URL-only items yet": "No URL-only items yet",
"RSS+Web url tab empty hint":
"Links from your RSS subscriptions appear under the RSS tab. This tab lists article URLs from Nostr (reactions, comments, bookmarks on web pages) when they are not only covered by feed items, and links you add manually.",
"Respond to this RSS entry": "Respond to this RSS entry", "Respond to this RSS entry": "Respond to this RSS entry",
"RSS read-only thread hint": "Nostr replies, zaps, and highlights are hidden here. Use this to add the article to your URL feed and respond there.", "RSS read-only thread hint": "Nostr replies, zaps, and highlights are hidden here. Use this to add the article to your URL feed and respond there.",
"RSS feed item label": "RSS", "RSS feed item label": "RSS",

77
src/lib/rss-web-feed.ts

@ -14,7 +14,7 @@ import {
} from '@/lib/rss-article' } from '@/lib/rss-article'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { isImage, isLocalNetworkUrl, isMedia, isVideo, normalizeUrl } from '@/lib/url' import { isImage, isLocalNetworkUrl, isMedia, isVideo, normalizeUrl } from '@/lib/url'
import { queryService } from '@/services/client.service' import { eventService, queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import type { RssFeedItem } from '@/services/rss-feed.service' import type { RssFeedItem } from '@/services/rss-feed.service'
import { isWebOnlyFauxRssItem } from '@/services/rss-feed.service' import { isWebOnlyFauxRssItem } from '@/services/rss-feed.service'
@ -479,6 +479,72 @@ function extractArticleUrlFromWebActivityEvent(evt: Event): string | undefined {
return undefined return undefined
} }
function touchRssWebDiscoveryUrlFromEvent(
evt: Event,
excludeClutter: boolean,
latestByUrl: Map<string, number>
): void {
const url = extractArticleUrlFromWebActivityEvent(evt)
if (!url) return
if (excludeClutter && isRssWebUnifiedClutterUrl(url)) return
const key = canonicalizeRssArticleUrl(url)
const prev = latestByUrl.get(key) ?? 0
if (evt.created_at > prev) latestByUrl.set(key, evt.created_at)
}
/** Merge manual / discovered URL lists; per URL keep the newest `addedAt`. */
export function mergeManualRssWebUrlEntries(...parts: ManualRssWebUrlEntry[]): ManualRssWebUrlEntry[] {
const byUrl = new Map<string, number>()
for (const list of parts) {
for (const e of list) {
const prev = byUrl.get(e.url) ?? 0
if (e.addedAt > prev) byUrl.set(e.url, e.addedAt)
}
}
return [...byUrl.entries()].map(([url, addedAt]) => ({ url, addedAt }))
}
/**
* Article URLs from session LRU + event archive (same kinds as relay discovery), so the URL tab is not
* empty when relays return nothing but the client already saw reactions / bookmarks / etc.
*/
export async function discoverRssWebArticleUrlsFromLocalCaches(options?: {
excludeClutterUrls?: boolean
}): Promise<ManualRssWebUrlEntry[]> {
const excludeClutter = options?.excludeClutterUrls !== false
const sinceSec = Math.floor(Date.now() / 1000) - RSS_WEB_RELAY_DISCOVERY_SINCE_SEC
const latestByUrl = new Map<string, number>()
const sessionEv = eventService.listSessionEventsByKinds(RSS_WEB_RELAY_DISCOVERY_KINDS, {
since: sinceSec,
limit: 5000
})
for (const evt of sessionEv) {
touchRssWebDiscoveryUrlFromEvent(evt, excludeClutter, latestByUrl)
}
try {
const archived = await indexedDb.scanEventArchiveByKinds({
kinds: RSS_WEB_RELAY_DISCOVERY_KINDS,
since: sinceSec,
maxRowsScanned: 24_000,
maxMatches: 4000
})
for (const evt of archived) {
touchRssWebDiscoveryUrlFromEvent(evt, excludeClutter, latestByUrl)
}
} catch {
/* IDB unavailable */
}
const entries = [...latestByUrl.entries()].map(([url, addedAt]) => ({ url, addedAt }))
logger.info('[RssWebFeed] Local URL discovery finished', {
uniqueUrls: entries.length,
sessionHits: sessionEv.length
})
return entries
}
/** /**
* One REQ per kind, no `authors` filter: latest events from aggregated relays, grouped by canonical URL. * One REQ per kind, no `authors` filter: latest events from aggregated relays, grouped by canonical URL.
*/ */
@ -503,14 +569,7 @@ export async function fetchDiscoveredWebUrlsFromRelays(options: {
}) })
const latestByUrl = new Map<string, number>() const latestByUrl = new Map<string, number>()
const onEvent = (evt: Event) => { const onEvent = (evt: Event) => touchRssWebDiscoveryUrlFromEvent(evt, excludeClutter, latestByUrl)
const url = extractArticleUrlFromWebActivityEvent(evt)
if (!url) return
if (excludeClutter && isRssWebUnifiedClutterUrl(url)) return
const key = canonicalizeRssArticleUrl(url)
const prev = latestByUrl.get(key) ?? 0
if (evt.created_at > prev) latestByUrl.set(key, evt.created_at)
}
await Promise.all( await Promise.all(
RSS_WEB_RELAY_DISCOVERY_KINDS.map(async (kind) => { RSS_WEB_RELAY_DISCOVERY_KINDS.map(async (kind) => {

76
src/pages/primary/CalendarPrimaryPage.tsx

@ -176,7 +176,8 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
try { try {
const { rangeStartMs, rangeEndExclusiveMs } = paddedMonthRange const { rangeStartMs, rangeEndExclusiveMs } = paddedMonthRange
const [fromIdb, fromArchive] = await Promise.all([
const idbP = Promise.all([
indexedDb.getCalendarEventsForOccurrenceWindow( indexedDb.getCalendarEventsForOccurrenceWindow(
rangeStartMs, rangeStartMs,
rangeEndExclusiveMs, rangeEndExclusiveMs,
@ -189,30 +190,50 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
2500 2500
) )
]) ])
if (cancelled) return .then(([fromIdb, fromArchive]) =>
dedupeCalendarEventsPreferringOccurrenceRange(
const localBaseline = dedupeCalendarEventsPreferringOccurrenceRange( [...fromIdb, ...fromArchive],
[...fromIdb, ...fromArchive], rangeStartMs,
rangeStartMs, rangeEndExclusiveMs
rangeEndExclusiveMs )
) )
.catch((): NostrEvent[] => [])
const fromSessionNow = client.getSessionEventsMatchingSearch( const fromSessionNow = client.getSessionEventsMatchingSearch(
'', '',
SESSION_CALENDAR_MERGE_CAP, SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS] [...CALENDAR_EVENT_KINDS]
) )
setRawEvents( const sessionOnly = dedupeCalendarEventsPreferringOccurrenceRange(
dedupeCalendarEventsPreferringOccurrenceRange( fromSessionNow,
[...localBaseline, ...fromSessionNow], rangeStartMs,
rangeStartMs, rangeEndExclusiveMs
rangeEndExclusiveMs
)
) )
setLoading(false) if (!cancelled) {
setRawEvents(sessionOnly)
setLoading(false)
}
void idbP.then((localBaseline) => {
if (cancelled) return
const s2 = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents(
dedupeCalendarEventsPreferringOccurrenceRange(
[...localBaseline, ...s2],
rangeStartMs,
rangeEndExclusiveMs
)
)
})
if (!relayUrls.length) { if (!relayUrls.length) {
scheduleLateSessionMerge(localBaseline) void idbP.then((lb) => {
if (!cancelled) scheduleLateSessionMerge(lb)
})
return return
} }
@ -257,17 +278,18 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
) )
) )
let batch: NostrEvent[] = [] const relayMergedP = Promise.all([mainReq, ...chunkReqs])
const fromFollowing: NostrEvent[] = [] .then((merged) => {
try { const batch = merged[0] ?? []
const merged = await Promise.all([mainReq, ...chunkReqs]) const fromFollowing: NostrEvent[] = []
batch = merged[0] ?? [] for (let i = 1; i < merged.length; i++) {
for (let i = 1; i < merged.length; i++) { fromFollowing.push(...(merged[i] ?? []))
fromFollowing.push(...(merged[i] ?? [])) }
} return { batch, fromFollowing }
} catch { })
/* keep IndexedDB + session view; relays may be unreachable */ .catch(() => ({ batch: [] as NostrEvent[], fromFollowing: [] as NostrEvent[] }))
}
const [{ batch, fromFollowing }, localBaseline] = await Promise.all([relayMergedP, idbP])
if (cancelled) return if (cancelled) return
const fromSession = client.getSessionEventsMatchingSearch( const fromSession = client.getSessionEventsMatchingSearch(

26
src/pages/primary/SpellsPage/index.tsx

@ -259,12 +259,14 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
if (!cancelled) setFollowSetListEvents([]) if (!cancelled) setFollowSetListEvents([])
return return
} }
const events = await queryService.fetchEvents( const [events, tombstones] = await Promise.all([
feedUrls, queryService.fetchEvents(
{ authors: [pubkey], kinds: [ExtendedKind.FOLLOW_SET], limit: 500 }, feedUrls,
{ eoseTimeout: 2000, globalTimeout: 15000, firstRelayResultGraceMs: false } { authors: [pubkey], kinds: [ExtendedKind.FOLLOW_SET], limit: 500 },
) { eoseTimeout: 2000, globalTimeout: 15000, firstRelayResultGraceMs: false }
const tombstones = await indexedDb.getAllTombstones() ),
indexedDb.getAllTombstones()
])
if (!cancelled) { if (!cancelled) {
setFollowSetListEvents(dedupeFollowSetEventsByD(filterEventsExcludingTombstones(events, tombstones))) setFollowSetListEvents(dedupeFollowSetEventsByD(filterEventsExcludingTombstones(events, tombstones)))
} }
@ -349,12 +351,14 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
if (manualBump) { if (manualBump) {
spellCatalogLastManualKeyRef.current = spellCatalogManualRefreshKey spellCatalogLastManualKeyRef.current = spellCatalogManualRefreshKey
} }
const cachedSpells = await indexedDb.getSpellEvents()
if (cancelled) return
const shouldSyncFromRelays = manualBump || cachedSpells.length === 0 const idbSpellsP = indexedDb.getSpellEvents()
if (!shouldSyncFromRelays) { if (!manualBump) {
return const cachedSpells = await idbSpellsP
if (cancelled) return
if (cachedSpells.length > 0) {
return
}
} }
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), { const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {

26
src/pages/secondary/ProfileInteractionDiagramPage/index.tsx

@ -55,16 +55,24 @@ function useProfileInteractionPartners(authorPubkey: string | undefined, refresh
const kindsArr = [...INTERACTION_KINDS] const kindsArr = [...INTERACTION_KINDS]
const sessionEv = eventService.listSessionEventsAuthoredBy(pk, { kinds: kindsArr, limit: 900 }) const sessionEv = eventService.listSessionEventsAuthoredBy(pk, { kinds: kindsArr, limit: 900 })
setSessionEventCount(sessionEv.length) setSessionEventCount(sessionEv.length)
setArchiveAuthorEvents(0)
const mergedSession = mergeEventsById([...sessionEv])
setPartners(buildInteractionPartnerStats(mergedSession, pk))
const idbEv = await indexedDb.scanEventArchiveByAuthorPubkey(pk, { void (async () => {
kinds: kindsArr, try {
maxRowsScanned: 14_000, const idbEv = await indexedDb.scanEventArchiveByAuthorPubkey(pk, {
maxMatches: 450 kinds: kindsArr,
}) maxRowsScanned: 14_000,
setArchiveAuthorEvents(idbEv.length) maxMatches: 450
})
const merged = mergeEventsById([...sessionEv, ...idbEv]) setArchiveAuthorEvents(idbEv.length)
setPartners(buildInteractionPartnerStats(merged, pk)) const merged = mergeEventsById([...sessionEv, ...idbEv])
setPartners(buildInteractionPartnerStats(merged, pk))
} catch {
/* best-effort disk */
}
})()
} finally { } finally {
setLoading(false) setLoading(false)
} }

43
src/services/mention-event-search.service.ts

@ -82,29 +82,31 @@ async function searchCitationEventsForPickerInternal(
} }
const idHex = tryParseCitationEventIdFromQuery(q) const idHex = tryParseCitationEventIdFromQuery(q)
if (idHex) {
const ev = await client.fetchEvent(idHex)
if (ev && kindsList.includes(ev.kind)) push(ev, false)
if (out.length >= limit) return out.slice(0, limit)
}
for (const ev of eventService.getSessionCitationFieldSearch(q, limit)) { for (const ev of eventService.getSessionCitationFieldSearch(q, limit)) {
push(ev, false) push(ev, false)
if (out.length >= limit) return out.slice(0, limit) if (out.length >= limit) return out.slice(0, limit)
} }
const fromArch = await indexedDb.getCachedAndArchivedCitationFieldSearch( const needAfterSession = limit - out.length
q, const [idEv, fromArch, relayUrls] = await Promise.all([
limit - out.length, idHex ? client.fetchEvent(idHex) : Promise.resolve(null),
kindsList, needAfterSession > 0
{ archiveScanMaxMs: 14_000 } ? indexedDb.getCachedAndArchivedCitationFieldSearch(q, needAfterSession, kindsList, {
) archiveScanMaxMs: 14_000
})
: Promise.resolve([] as NEvent[]),
buildCitationPickerSearchRelayUrls()
])
if (idEv && kindsList.includes(idEv.kind)) push(idEv, false)
if (out.length >= limit) return out.slice(0, limit)
for (const ev of fromArch) { for (const ev of fromArch) {
push(ev, false) push(ev, false)
if (out.length >= limit) return out.slice(0, limit) if (out.length >= limit) return out.slice(0, limit)
} }
const relayUrls = await buildCitationPickerSearchRelayUrls()
const need = limit - out.length const need = limit - out.length
if (need <= 0) return out.slice(0, limit) if (need <= 0) return out.slice(0, limit)
@ -170,15 +172,16 @@ export async function searchEventsForPicker(
fromSession.forEach(addUnique) fromSession.forEach(addUnique)
if (out.length >= limit) return out.slice(0, limit) if (out.length >= limit) return out.slice(0, limit)
const fromIdb = await indexedDb.getCachedEventsForSearch(q, limit - out.length, kindsList) const need = limit - out.length
const [fromIdb, fromRelays] = await Promise.all([
indexedDb.getCachedEventsForSearch(q, need, kindsList),
queryService.fetchEvents(
SEARCHABLE_RELAY_URLS,
{ kinds: kindsList, search: q, limit: need },
{ eoseTimeout: 5000, globalTimeout: 8000 }
)
])
fromIdb.forEach(addUnique) fromIdb.forEach(addUnique)
if (out.length >= limit) return out.slice(0, limit)
const fromRelays = await queryService.fetchEvents(
SEARCHABLE_RELAY_URLS,
{ kinds: kindsList, search: q, limit: limit - out.length },
{ eoseTimeout: 5000, globalTimeout: 8000 }
)
fromRelays.forEach(addUnique) fromRelays.forEach(addUnique)
return out.slice(0, limit) return out.slice(0, limit)
} }

Loading…
Cancel
Save