Browse Source

refine library storage

imwald
Silberengel 1 week ago
parent
commit
7ed700199b
  1. 7
      src/components/CacheBrowser/CacheBrowserDialog.tsx
  2. 92
      src/components/LibraryIndexCacheSettings/index.tsx
  3. 2
      src/components/Note/PublicationCard.tsx
  4. 5
      src/hooks/useLibraryPublications.ts
  5. 17
      src/i18n/locales/de.ts
  6. 17
      src/i18n/locales/en.ts
  7. 19
      src/lib/library-publication-index.ts
  8. 2
      src/pages/secondary/CacheSettingsPage/index.tsx

7
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 { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import indexedDb, { isLikelyCachedNostrEvent, StoreNames, type TCachedEventSearchHit } from '@/services/indexed-db.service' 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
@ -177,7 +178,11 @@ export default function CacheBrowserDialog({
if (!selectedStore) return if (!selectedStore) return
if (!confirm(t('Are you sure you want to delete all items from this store?'))) return if (!confirm(t('Are you sure you want to delete all items from this store?'))) return
try { try {
await indexedDb.clearStore(selectedStore) if (selectedStore === StoreNames.LIBRARY_PUBLICATION_INDEX) {
await clearAllLibraryIndexCaches()
} else {
await indexedDb.clearStore(selectedStore)
}
setStoreItems([]) setStoreItems([])
void loadCacheInfo() void loadCacheInfo()
toast.success(t('All items deleted successfully')) toast.success(t('All items deleted successfully'))

92
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 (
<div className="mt-8 space-y-4 border-t border-border pt-6">
<h3 className="text-base font-medium">{t('libraryIndexCache.sectionTitle')}</h3>
<p className="text-muted-foreground text-sm">{t('libraryIndexCache.sectionBlurb')}</p>
<p className="text-muted-foreground text-xs">{defaultsHint}</p>
<p className="text-muted-foreground text-xs">
{t('libraryIndexCache.footprintSummary', {
count: footprint?.count ?? 0,
mb: formatMb(footprint?.bytes ?? 0),
maxEntries: budget.maxEntries,
maxMb: Math.round(budget.maxBytes / (1024 * 1024))
})}
</p>
<Button type="button" variant="secondary" disabled={clearing} onClick={() => void handleClear()}>
{clearing ? t('libraryIndexCache.clearing') : t('libraryIndexCache.clear')}
</Button>
</div>
)
}

2
src/components/Note/PublicationCard.tsx

@ -9,6 +9,7 @@ import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import Image from '../Image' import Image from '../Image'
import { extractBookMetadata } from '@/lib/bookstr-parser' import { extractBookMetadata } from '@/lib/bookstr-parser'
import { persistLibraryPublicationForReading } from '@/lib/library-publication-index'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
export default function PublicationCard({ export default function PublicationCard({
@ -36,6 +37,7 @@ export default function PublicationCard({
const handleCardClick = (e: React.MouseEvent) => { const handleCardClick = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
if (disableNavigation) return if (disableNavigation) return
persistLibraryPublicationForReading(event)
navigateToNote(toNote(event), event) navigateToNote(toNote(event), event)
} }

5
src/hooks/useLibraryPublications.ts

@ -1,5 +1,5 @@
import { import {
clearLibraryPublicationIndexCache, clearAllLibraryIndexCaches,
filterLibraryPublicationsBySearch, filterLibraryPublicationsBySearch,
filterLibraryPublicationsByUser, filterLibraryPublicationsByUser,
buildLibraryRelayUrls, buildLibraryRelayUrls,
@ -81,8 +81,7 @@ export function useLibraryPublications(isActive: boolean) {
}, [isActive, load]) }, [isActive, load])
const refresh = useCallback(() => { const refresh = useCallback(() => {
clearLibraryPublicationIndexCache() void clearAllLibraryIndexCaches().then(() => load(true))
void load(true)
}, [load]) }, [load])
const filteredEntries = useMemo(() => { const filteredEntries = useMemo(() => {

17
src/i18n/locales/de.ts

@ -1658,6 +1658,23 @@ export default {
'Library badge label': 'Label', 'Library badge label': 'Label',
'Library badge comment': 'Kommentar', 'Library badge comment': 'Kommentar',
'Library badge highlight': 'Markierung', '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': 'Leeren',
'Search page clear description': 'Search page clear description':
'Suchfeld leeren, Vorschläge schließen und Ergebnisse entfernen, um neu zu suchen.', 'Suchfeld leeren, Vorschläge schließen und Ergebnisse entfernen, um neu zu suchen.',

17
src/i18n/locales/en.ts

@ -1681,6 +1681,23 @@ export default {
'Library badge label': 'Label', 'Library badge label': 'Label',
'Library badge comment': 'Comment', 'Library badge comment': 'Comment',
'Library badge highlight': 'Highlight', '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': 'Clear',
'Search page clear description': 'Search page clear description':
'Clear the search field, close suggestions, and remove results so you can start a new search.', 'Clear the search field, close suggestions, and remove results so you can start a new search.',

19
src/lib/library-publication-index.ts

@ -12,9 +12,12 @@ import {
} from '@/lib/publication-index' } from '@/lib/publication-index'
import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' import { buildComprehensiveRelayList } from '@/lib/relay-list-builder'
import { import {
clearLibraryIndexIdbCache,
loadLibraryIndexCacheEvents, loadLibraryIndexCacheEvents,
persistLibraryIndexCacheEvents persistLibraryIndexCacheEvents
} from '@/lib/library-index-idb-cache' } from '@/lib/library-index-idb-cache'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { import {
canonicalRelaySessionKey, canonicalRelaySessionKey,
httpIndexBasesForRelayQuery, httpIndexBasesForRelayQuery,
@ -599,3 +602,19 @@ export async function loadLibraryPublicationIndex(
export function clearLibraryPublicationIndexCache(): void { export function clearLibraryPublicationIndexCache(): void {
sessionCache = null sessionCache = null
} }
/** Clears Library tab session + IDB index cache only (publication reading cache is unchanged). */
export async function clearAllLibraryIndexCaches(): Promise<void> {
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(() => {})
}

2
src/pages/secondary/CacheSettingsPage/index.tsx

@ -1,6 +1,7 @@
import CacheEventImportSettings from '@/components/CacheEventImportSettings' import CacheEventImportSettings from '@/components/CacheEventImportSettings'
import InBrowserCacheSetting from '@/components/InBrowserCacheSetting' import InBrowserCacheSetting from '@/components/InBrowserCacheSetting'
import EventArchiveCacheSettings from '@/components/EventArchiveCacheSettings' import EventArchiveCacheSettings from '@/components/EventArchiveCacheSettings'
import LibraryIndexCacheSettings from '@/components/LibraryIndexCacheSettings'
import PrivateKeyRecoverySetting from '@/components/PrivateKeyRecoverySetting' import PrivateKeyRecoverySetting from '@/components/PrivateKeyRecoverySetting'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
@ -65,6 +66,7 @@ const CacheSettingsPage = forwardRef<TPageRef, { index?: number; hideTitlebar?:
<InBrowserCacheSetting /> <InBrowserCacheSetting />
<CacheEventImportSettings /> <CacheEventImportSettings />
<EventArchiveCacheSettings /> <EventArchiveCacheSettings />
<LibraryIndexCacheSettings />
</div> </div>
</SecondaryPageLayout> </SecondaryPageLayout>
) )

Loading…
Cancel
Save