From 7ed700199b8c0329bc0e701f464afdfcc3a2fb9a Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 7 Jun 2026 16:50:19 +0200 Subject: [PATCH] refine library storage --- .../CacheBrowser/CacheBrowserDialog.tsx | 7 +- .../LibraryIndexCacheSettings/index.tsx | 92 +++++++++++++++++++ src/components/Note/PublicationCard.tsx | 2 + src/hooks/useLibraryPublications.ts | 5 +- src/i18n/locales/de.ts | 17 ++++ src/i18n/locales/en.ts | 17 ++++ src/lib/library-publication-index.ts | 19 ++++ .../secondary/CacheSettingsPage/index.tsx | 2 + 8 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 src/components/LibraryIndexCacheSettings/index.tsx diff --git a/src/components/CacheBrowser/CacheBrowserDialog.tsx b/src/components/CacheBrowser/CacheBrowserDialog.tsx index 0fe0e75c..d5aebaba 100644 --- a/src/components/CacheBrowser/CacheBrowserDialog.tsx +++ b/src/components/CacheBrowser/CacheBrowserDialog.tsx @@ -6,6 +6,7 @@ import { Trash2, RefreshCw, Database, WrapText, Search, X, TriangleAlert, Copy, import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' import indexedDb, { isLikelyCachedNostrEvent, StoreNames, type TCachedEventSearchHit } from '@/services/indexed-db.service' +import { clearAllLibraryIndexCaches } from '@/lib/library-publication-index' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' @@ -177,7 +178,11 @@ export default function CacheBrowserDialog({ if (!selectedStore) return if (!confirm(t('Are you sure you want to delete all items from this store?'))) return try { - await indexedDb.clearStore(selectedStore) + if (selectedStore === StoreNames.LIBRARY_PUBLICATION_INDEX) { + await clearAllLibraryIndexCaches() + } else { + await indexedDb.clearStore(selectedStore) + } setStoreItems([]) void loadCacheInfo() toast.success(t('All items deleted successfully')) diff --git a/src/components/LibraryIndexCacheSettings/index.tsx b/src/components/LibraryIndexCacheSettings/index.tsx new file mode 100644 index 00000000..d2acb2d2 --- /dev/null +++ b/src/components/LibraryIndexCacheSettings/index.tsx @@ -0,0 +1,92 @@ +import { Button } from '@/components/ui/button' +import { getLibraryIndexCacheFootprint, getLibraryIndexCacheBudget } from '@/lib/library-index-idb-cache' +import { clearAllLibraryIndexCaches } from '@/lib/library-publication-index' +import { isImwaldElectron, isMobileBrowserProfile } from '@/lib/client-platform' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' + +function formatMb(bytes: number): string { + return (bytes / (1024 * 1024)).toFixed(1) +} + +function platformLabel(): string { + if (isImwaldElectron()) return 'desktop-app' + if (isMobileBrowserProfile()) return 'mobile-web' + return 'desktop-web' +} + +export default function LibraryIndexCacheSettings() { + const { t } = useTranslation() + const [footprint, setFootprint] = useState<{ count: number; bytes: number } | null>(null) + const [clearing, setClearing] = useState(false) + + const budget = useMemo(() => getLibraryIndexCacheBudget(), []) + + const refreshFootprint = useCallback(async () => { + try { + setFootprint(await getLibraryIndexCacheFootprint()) + } catch { + setFootprint({ count: 0, bytes: 0 }) + } + }, []) + + useEffect(() => { + void refreshFootprint() + }, [refreshFootprint]) + + const handleClear = async () => { + if (!confirm(t('libraryIndexCache.clearConfirm'))) return + setClearing(true) + try { + await clearAllLibraryIndexCaches() + await refreshFootprint() + toast.success(t('libraryIndexCache.clearedToast')) + } catch { + toast.error(t('libraryIndexCache.clearFailed')) + } finally { + setClearing(false) + } + } + + const defaultsHint = useMemo(() => { + const p = platformLabel() + if (p === 'mobile-web') { + return t('libraryIndexCache.defaultsMobile', { + entries: budget.maxEntries, + mb: Math.round(budget.maxBytes / (1024 * 1024)) + }) + } + if (p === 'desktop-app') { + return t('libraryIndexCache.defaultsElectron', { + entries: budget.maxEntries, + mb: Math.round(budget.maxBytes / (1024 * 1024)) + }) + } + return t('libraryIndexCache.defaultsDesktopWeb', { + entries: budget.maxEntries, + mb: Math.round(budget.maxBytes / (1024 * 1024)) + }) + }, [budget.maxBytes, budget.maxEntries, t]) + + return ( +
+

{t('libraryIndexCache.sectionTitle')}

+

{t('libraryIndexCache.sectionBlurb')}

+

{defaultsHint}

+ +

+ {t('libraryIndexCache.footprintSummary', { + count: footprint?.count ?? 0, + mb: formatMb(footprint?.bytes ?? 0), + maxEntries: budget.maxEntries, + maxMb: Math.round(budget.maxBytes / (1024 * 1024)) + })} +

+ + +
+ ) +} diff --git a/src/components/Note/PublicationCard.tsx b/src/components/Note/PublicationCard.tsx index c74c1d9c..9e849d3c 100644 --- a/src/components/Note/PublicationCard.tsx +++ b/src/components/Note/PublicationCard.tsx @@ -9,6 +9,7 @@ import { Event, kinds } from 'nostr-tools' import { useMemo } from 'react' import Image from '../Image' import { extractBookMetadata } from '@/lib/bookstr-parser' +import { persistLibraryPublicationForReading } from '@/lib/library-publication-index' import { ExtendedKind } from '@/constants' export default function PublicationCard({ @@ -36,6 +37,7 @@ export default function PublicationCard({ const handleCardClick = (e: React.MouseEvent) => { e.stopPropagation() if (disableNavigation) return + persistLibraryPublicationForReading(event) navigateToNote(toNote(event), event) } diff --git a/src/hooks/useLibraryPublications.ts b/src/hooks/useLibraryPublications.ts index 9663472e..2cfd782b 100644 --- a/src/hooks/useLibraryPublications.ts +++ b/src/hooks/useLibraryPublications.ts @@ -1,5 +1,5 @@ import { - clearLibraryPublicationIndexCache, + clearAllLibraryIndexCaches, filterLibraryPublicationsBySearch, filterLibraryPublicationsByUser, buildLibraryRelayUrls, @@ -81,8 +81,7 @@ export function useLibraryPublications(isActive: boolean) { }, [isActive, load]) const refresh = useCallback(() => { - clearLibraryPublicationIndexCache() - void load(true) + void clearAllLibraryIndexCaches().then(() => load(true)) }, [load]) const filteredEntries = useMemo(() => { diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 6d22d7f5..2d9f7a96 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -1658,6 +1658,23 @@ export default { 'Library badge label': 'Label', 'Library badge comment': 'Kommentar', 'Library badge highlight': 'Markierung', + 'libraryIndexCache.sectionTitle': 'Bibliotheks-Publikationsindex', + 'libraryIndexCache.sectionBlurb': + 'Zwischengespeicherte Kind-30040-Index-Events für den Bibliotheks-Tab. Beim Leeren wird nur der Entdeckungslisten-Cache entfernt — geöffnete Publikationen bleiben im Lese-Cache.', + 'libraryIndexCache.defaultsMobile': + 'Standard mobil: bis zu {{entries}} Indizes, ~{{mb}} MB.', + 'libraryIndexCache.defaultsElectron': + 'Standard Desktop-App: bis zu {{entries}} Indizes, ~{{mb}} MB.', + 'libraryIndexCache.defaultsDesktopWeb': + 'Standard Desktop-Web: bis zu {{entries}} Indizes, ~{{mb}} MB.', + 'libraryIndexCache.footprintSummary': + '{{count}} / {{maxEntries}} Indizes (~{{mb}} / {{maxMb}} MB).', + 'libraryIndexCache.clear': 'Bibliotheksindex-Cache leeren', + 'libraryIndexCache.clearing': 'Wird geleert…', + 'libraryIndexCache.clearConfirm': + 'Bibliotheksindex-Cache leeren? Beim nächsten Besuch werden Indizes erneut von Relays geladen. Geöffnete Publikationen bleiben im Lese-Cache.', + 'libraryIndexCache.clearedToast': 'Bibliotheksindex-Cache geleert.', + 'libraryIndexCache.clearFailed': 'Bibliotheksindex-Cache konnte nicht geleert werden.', 'Search page clear': 'Leeren', 'Search page clear description': 'Suchfeld leeren, Vorschläge schließen und Ergebnisse entfernen, um neu zu suchen.', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 8b9dd865..b78e33e9 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1681,6 +1681,23 @@ export default { 'Library badge label': 'Label', 'Library badge comment': 'Comment', 'Library badge highlight': 'Highlight', + 'libraryIndexCache.sectionTitle': 'Library publication index', + 'libraryIndexCache.sectionBlurb': + 'Cached kind-30040 index events used to populate the Library tab. Clearing this only removes the discovery list cache—not publications you have opened for reading.', + 'libraryIndexCache.defaultsMobile': + 'Default on mobile web: up to {{entries}} indexes, ~{{mb}} MB.', + 'libraryIndexCache.defaultsElectron': + 'Default in the desktop app: up to {{entries}} indexes, ~{{mb}} MB.', + 'libraryIndexCache.defaultsDesktopWeb': + 'Default on desktop web: up to {{entries}} indexes, ~{{mb}} MB.', + 'libraryIndexCache.footprintSummary': + 'Using {{count}} / {{maxEntries}} indexes (~{{mb}} / {{maxMb}} MB).', + 'libraryIndexCache.clear': 'Clear library index cache', + 'libraryIndexCache.clearing': 'Clearing…', + 'libraryIndexCache.clearConfirm': + 'Clear the Library index cache? The Library tab will reload indexes from relays on next visit. Opened publications stay in your publication reading cache.', + 'libraryIndexCache.clearedToast': 'Library index cache cleared.', + 'libraryIndexCache.clearFailed': 'Failed to clear library index cache.', 'Search page clear': 'Clear', 'Search page clear description': 'Clear the search field, close suggestions, and remove results so you can start a new search.', diff --git a/src/lib/library-publication-index.ts b/src/lib/library-publication-index.ts index 62460e62..5c49e652 100644 --- a/src/lib/library-publication-index.ts +++ b/src/lib/library-publication-index.ts @@ -12,9 +12,12 @@ import { } from '@/lib/publication-index' import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' import { + clearLibraryIndexIdbCache, loadLibraryIndexCacheEvents, persistLibraryIndexCacheEvents } from '@/lib/library-index-idb-cache' +import client from '@/services/client.service' +import indexedDb from '@/services/indexed-db.service' import { canonicalRelaySessionKey, httpIndexBasesForRelayQuery, @@ -599,3 +602,19 @@ export async function loadLibraryPublicationIndex( export function clearLibraryPublicationIndexCache(): void { sessionCache = null } + +/** Clears Library tab session + IDB index cache only (publication reading cache is unchanged). */ +export async function clearAllLibraryIndexCaches(): Promise { + sessionCache = null + await clearLibraryIndexIdbCache() +} + +/** + * When opening a publication from Library, seed session cache and the publication events store + * so offline re-read works even if the index lived only in the Library LRU store. + */ +export function persistLibraryPublicationForReading(event: Event): void { + if (event.kind !== ExtendedKind.PUBLICATION) return + client.addEventToCache(event) + void indexedDb.putReplaceableEvent(event).catch(() => {}) +} diff --git a/src/pages/secondary/CacheSettingsPage/index.tsx b/src/pages/secondary/CacheSettingsPage/index.tsx index 1670c181..94c1c82a 100644 --- a/src/pages/secondary/CacheSettingsPage/index.tsx +++ b/src/pages/secondary/CacheSettingsPage/index.tsx @@ -1,6 +1,7 @@ import CacheEventImportSettings from '@/components/CacheEventImportSettings' import InBrowserCacheSetting from '@/components/InBrowserCacheSetting' import EventArchiveCacheSettings from '@/components/EventArchiveCacheSettings' +import LibraryIndexCacheSettings from '@/components/LibraryIndexCacheSettings' import PrivateKeyRecoverySetting from '@/components/PrivateKeyRecoverySetting' import { RefreshButton } from '@/components/RefreshButton' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' @@ -65,6 +66,7 @@ const CacheSettingsPage = forwardRef + )