Browse Source

fix mobile

imwald
Silberengel 1 week ago
parent
commit
534781a4c5
  1. 3
      src/components/Library/LibraryPublicationGrid.tsx
  2. 28
      src/components/Note/PublicationIndexBody.tsx
  3. 15
      src/hooks/useProgressivePublicationContent.tsx
  4. 27
      src/lib/library-index-idb-cache.ts
  5. 9
      src/lib/library-publication-index.ts
  6. 116
      src/services/indexed-db.service.ts

3
src/components/Library/LibraryPublicationGrid.tsx

@ -2,6 +2,7 @@ import PublicationCard from '@/components/Note/PublicationCard' @@ -2,6 +2,7 @@ import PublicationCard from '@/components/Note/PublicationCard'
import { Skeleton } from '@/components/ui/skeleton'
import { libraryPublicationGridColumnClass, usePanelMode } from '@/hooks/usePanelMode'
import type { LibraryPublicationEntry } from '@/lib/library-publication-index'
import { eventTagAddress } from '@/lib/publication-index'
import { isBooklistNip32Label } from '@/lib/nip32-label'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
@ -137,7 +138,7 @@ export default function LibraryPublicationGrid({ @@ -137,7 +138,7 @@ export default function LibraryPublicationGrid({
<div className={cn('grid gap-4', gridCols)}>
{entries.map((entry) => (
<div
key={entry.event.id}
key={eventTagAddress(entry.event) ?? entry.event.id}
className={cn(
'flex min-w-0 flex-col rounded-lg border border-border bg-card shadow-sm overflow-hidden'
)}

28
src/components/Note/PublicationIndexBody.tsx

@ -3,6 +3,7 @@ import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle' @@ -3,6 +3,7 @@ import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import NoteOptions from '@/components/NoteOptions'
import { DOCUMENT_RELAY_URLS, ExtendedKind, FAST_READ_RELAY_URLS, LIBRARY_RELAY_URLS } from '@/constants'
import { useProgressivePublicationContent } from '@/hooks/useProgressivePublicationContent'
import { useNearViewport } from '@/hooks/useNearViewport'
import { orderedPublicationRefsFromIndex } from '@/lib/publication-asciidoc-assembler'
import { publicationRefKey } from '@/lib/publication-section-fetch'
import {
@ -100,26 +101,13 @@ function PublicationSectionNodeView({ @@ -100,26 +101,13 @@ function PublicationSectionNodeView({
const isMissing = Boolean(refKey && failedKeys.has(refKey))
const isLoading = Boolean(refKey && loadingKeys.has(refKey))
const needsLoad = Boolean(refKey && !node.event && !isMissing && !isLoading)
const isNear = useNearViewport(sectionElRef, { enabled: needsLoad, marginPx: 480 })
useEffect(() => {
if (!needsLoad) return
const el = sectionElRef.current
if (!el) return
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
if (!needsLoad || !isNear) return
onRequestLoad(node.ref, node.indexEvent)
onReadAhead()
}
},
{ rootMargin: '720px 0px 480px 0px', threshold: 0 }
)
observer.observe(el)
return () => observer.disconnect()
}, [needsLoad, node.ref, node.indexEvent, onRequestLoad, onReadAhead])
}, [needsLoad, isNear, node.ref, node.indexEvent, onRequestLoad, onReadAhead])
return (
<section
@ -179,9 +167,13 @@ function PublicationTableOfContents({ @@ -179,9 +167,13 @@ function PublicationTableOfContents({
}) {
const { t } = useTranslation()
const scrollToSection = useCallback((id: string) => {
const scrollToSection = useCallback(
(id: string) => {
if (!readingStarted) return
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, [])
},
[readingStarted]
)
if (entries.length === 0) return null

15
src/hooks/useProgressivePublicationContent.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { isMobileBrowserProfile } from '@/lib/client-platform'
import { indexPublicationEvents } from '@/lib/publication-asciidoc-assembler'
import {
collectPendingPublicationSectionLoads,
@ -8,8 +9,11 @@ import { publicationRefKey, type PublicationSectionRef } from '@/lib/publication @@ -8,8 +9,11 @@ import { publicationRefKey, type PublicationSectionRef } from '@/lib/publication
import type { Event } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react'
const INITIAL_PREFETCH_COUNT = 3
const READ_AHEAD_COUNT = 1
const READ_AHEAD_COUNT = 2
function initialPrefetchCount(): number {
return isMobileBrowserProfile() ? 8 : 5
}
export function useProgressivePublicationContent(
rootIndex: Event,
@ -113,7 +117,7 @@ export function useProgressivePublicationContent( @@ -113,7 +117,7 @@ export function useProgressivePublicationContent(
failedRef.current,
inFlightRef.current
)
for (const task of pending.slice(0, INITIAL_PREFETCH_COUNT)) {
for (const task of pending.slice(0, initialPrefetchCount())) {
if (cancelled) return
await loadSection(task.ref, task.indexEvent)
}
@ -134,6 +138,11 @@ export function useProgressivePublicationContent( @@ -134,6 +138,11 @@ export function useProgressivePublicationContent(
prefetchTasks(pending.slice(0, READ_AHEAD_COUNT))
}, [prefetchTasks, rootIndex])
useEffect(() => {
if (!enabled) return
readAhead()
}, [enabled, fetched, failedKeys, readAhead])
return {
fetched,
failedKeys,

27
src/lib/library-index-idb-cache.ts

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
import { ExtendedKind } from '@/constants'
import {
approxLibraryIndexEventBytes,
getLibraryIndexCacheBudget
@ -12,6 +11,13 @@ import { @@ -12,6 +11,13 @@ import {
import indexedDb from '@/services/indexed-db.service'
import type { Event } from 'nostr-tools'
type PersistLibraryIndexCacheOptions = {
/** When false, merge rows only — skip reconcile so partial batches cannot wipe unrelated cache rows. */
reconcile?: boolean
}
let persistQueue: Promise<void> = Promise.resolve()
export async function loadLibraryIndexCacheEvents(): Promise<Event[]> {
try {
const cached = await indexedDb.getLibraryPublicationIndexCacheEvents()
@ -22,8 +28,9 @@ export async function loadLibraryIndexCacheEvents(): Promise<Event[]> { @@ -22,8 +28,9 @@ export async function loadLibraryIndexCacheEvents(): Promise<Event[]> {
if (structural.length < cached.length) {
void indexedDb.pruneUnverifiedLibraryPublicationIndexCacheEvents().catch(() => {})
}
if (normalized.length !== cached.length) {
void persistLibraryIndexCacheEvents(normalized).catch(() => {})
const hasLegacyKeys = await indexedDb.libraryPublicationIndexCacheHasLegacyKeys()
if (normalized.length !== cached.length || hasLegacyKeys) {
void persistLibraryIndexCacheEvents(normalized, { reconcile: true }).catch(() => {})
}
return normalized
} catch (e) {
@ -36,14 +43,22 @@ export async function loadLibraryIndexCacheEvents(): Promise<Event[]> { @@ -36,14 +43,22 @@ export async function loadLibraryIndexCacheEvents(): Promise<Event[]> {
}
}
export async function persistLibraryIndexCacheEvents(events: Event[]): Promise<void> {
export async function persistLibraryIndexCacheEvents(
events: Event[],
options?: PersistLibraryIndexCacheOptions
): Promise<void> {
const map = buildStructuralPublicationIndexMap(filterStructuralIndexEvents(events))
const normalized = publicationIndexMapValues(map)
if (normalized.length === 0) return
const reconcile = options?.reconcile !== false
const run = async () => {
try {
const budget = getLibraryIndexCacheBudget()
await indexedDb.mergeLibraryPublicationIndexCacheEvents(normalized, budget)
if (reconcile) {
await indexedDb.reconcileLibraryPublicationIndexCache(map)
}
} catch (e) {
if (import.meta.env.DEV) {
logger.warn('[Library] index IDB write failed', {
@ -53,6 +68,10 @@ export async function persistLibraryIndexCacheEvents(events: Event[]): Promise<v @@ -53,6 +68,10 @@ export async function persistLibraryIndexCacheEvents(events: Event[]): Promise<v
}
}
persistQueue = persistQueue.then(run, run)
return persistQueue
}
export async function getLibraryIndexCacheFootprint(): Promise<{ count: number; bytes: number }> {
try {
return await indexedDb.getLibraryPublicationIndexCacheFootprint()

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

@ -631,7 +631,7 @@ export async function fetchLibraryIndexEvents( @@ -631,7 +631,7 @@ export async function fetchLibraryIndexEvents(
)
indexMap = await mergeValidIndexBatch(indexMap, knownValidIds, firstPageNetwork)
validMerged = publicationIndexMapValues(indexMap)
void persistLibraryIndexCacheEvents(validMerged)
void persistLibraryIndexCacheEvents(validMerged, { reconcile: false })
emitProgress()
if (import.meta.env.DEV) {
@ -1901,13 +1901,12 @@ export async function searchLibraryPublicationsOnRelays( @@ -1901,13 +1901,12 @@ export async function searchLibraryPublicationsOnRelays(
const settled = await Promise.all(batches)
const networkEvents = dedupeEventsById(settled.flat())
const valid = filterValidIndexEvents(networkEvents)
if (valid.length > 0) {
void persistLibraryIndexCacheEvents(valid)
}
const mergedIndex = publicationIndexMapValues(
mergePublicationIndexMaps(buildStructuralPublicationIndexMap(context.indexEvents ?? []), valid)
)
if (valid.length > 0 || mergedIndex.length > 0) {
void persistLibraryIndexCacheEvents(mergedIndex)
}
const indexByAddress = buildIndexByAddress(mergedIndex)
const roots = searchLibraryPublicationIndex(q, mergedIndex, indexByAddress)
const engagement = context.engagement ?? EMPTY_ENGAGEMENT

116
src/services/indexed-db.service.ts

@ -237,7 +237,7 @@ const FULL_TEXT_NOTE_SEARCH_STORES: ReadonlySet<string> = new Set([ @@ -237,7 +237,7 @@ const FULL_TEXT_NOTE_SEARCH_STORES: ReadonlySet<string> = new Set([
const ARCHIVE_CALENDAR_PURGE_SETTING_KEY = 'archiveCalendarPurgedV37'
/** Schema version we expect. When adding stores or migrations, bump this. */
const DB_VERSION = 40
const DB_VERSION = 41
/** Hint age for profile/payment reads (stale rows still returned; background refresh). */
const PROFILE_AND_PAYMENT_STALE_READ_MS = 5 * 60 * 1000
@ -261,6 +261,66 @@ type TLibraryPublicationIndexCacheRow = { @@ -261,6 +261,66 @@ type TLibraryPublicationIndexCacheRow = {
approxBytes: number
}
function approxLibraryPublicationIndexRowBytes(ev: Event): number {
try {
return new Blob([JSON.stringify(ev)]).size
} catch {
return 2048
}
}
/** v41: re-key library index rows from event id to kind:pubkey:d; dedupe by address. */
function migrateLibraryPublicationIndexCacheToAddressKeys(transaction: IDBTransaction): void {
const store = transaction.objectStore(StoreNames.LIBRARY_PUBLICATION_INDEX)
const rows: TLibraryPublicationIndexCacheRow[] = []
const readReq = store.openCursor()
readReq.onsuccess = () => {
const cursor = readReq.result
if (cursor) {
rows.push(cursor.value as TLibraryPublicationIndexCacheRow)
cursor.continue()
return
}
const byAddress = new Map<string, TLibraryPublicationIndexCacheRow>()
for (const row of rows) {
const ev = row?.value
if (!ev || ev.kind !== ExtendedKind.PUBLICATION || !isStructuralPublicationIndex(ev)) continue
const addr = eventTagAddress(ev)
if (!addr) continue
const existing = byAddress.get(addr)
if (!existing) {
byAddress.set(addr, {
key: addr,
value: ev,
addedAt: row.addedAt ?? Date.now(),
lastAccessAt: row.lastAccessAt ?? row.addedAt ?? Date.now(),
approxBytes: row.approxBytes ?? approxLibraryPublicationIndexRowBytes(ev)
})
continue
}
const winner = pickNewerPublicationIndexEvent(existing.value, ev)
byAddress.set(addr, {
key: addr,
value: winner,
addedAt: Math.min(existing.addedAt, row.addedAt ?? existing.addedAt),
lastAccessAt: Math.max(existing.lastAccessAt, row.lastAccessAt ?? row.addedAt ?? 0),
approxBytes: approxLibraryPublicationIndexRowBytes(winner)
})
}
const clearReq = store.clear()
clearReq.onsuccess = () => {
for (const row of byAddress.values()) {
store.put(row)
}
}
}
}
/** Create any object stores from {@link StoreNames} that are missing (e.g. after partial upgrades). */
function ensureMissingObjectStores(db: IDBDatabase): void {
for (const storeName of Object.values(StoreNames)) {
@ -536,6 +596,12 @@ class IndexedDbService { @@ -536,6 +596,12 @@ class IndexedDbService {
lib.createIndex('lastAccessAt', 'lastAccessAt', { unique: false })
}
}
if (event.oldVersion < 41) {
const tx = (event.target as IDBOpenDBRequest).transaction
if (tx && db.objectStoreNames.contains(StoreNames.LIBRARY_PUBLICATION_INDEX)) {
migrateLibraryPublicationIndexCacheToAddressKeys(tx)
}
}
ensureMissingObjectStores(db)
}
}
@ -3731,6 +3797,45 @@ class IndexedDbService { @@ -3731,6 +3797,45 @@ class IndexedDbService {
return toDelete.length
}
/** True when any row is keyed by event id instead of kind:pubkey:d address. */
async libraryPublicationIndexCacheHasLegacyKeys(): Promise<boolean> {
await this.initPromise
if (!this.db?.objectStoreNames.contains(StoreNames.LIBRARY_PUBLICATION_INDEX)) return false
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(StoreNames.LIBRARY_PUBLICATION_INDEX, 'readonly')
const req = tx.objectStore(StoreNames.LIBRARY_PUBLICATION_INDEX).openCursor()
req.onsuccess = () => {
const cursor = req.result as IDBCursorWithValue | null
if (!cursor) {
tx.commit()
resolve(false)
return
}
const rowKey = cursor.key as string
const row = cursor.value as TLibraryPublicationIndexCacheRow
const ev = row?.value
const addr = ev ? eventTagAddress(ev) : null
if (
!ev ||
ev.kind !== ExtendedKind.PUBLICATION ||
!isStructuralPublicationIndex(ev) ||
!addr ||
rowKey !== addr
) {
tx.commit()
resolve(true)
return
}
cursor.continue()
}
req.onerror = (e) => {
tx.commit()
reject(idbEventToError(e))
}
})
}
async getLibraryPublicationIndexCacheEvents(): Promise<Event[]> {
await this.initPromise
if (!this.db?.objectStoreNames.contains(StoreNames.LIBRARY_PUBLICATION_INDEX)) return []
@ -3880,15 +3985,14 @@ class IndexedDbService { @@ -3880,15 +3985,14 @@ class IndexedDbService {
const ev = row?.value
const addr = ev ? eventTagAddress(ev) : null
const canon = addr ? canonical.get(addr) : undefined
if (
const invalid =
!ev ||
ev.kind !== ExtendedKind.PUBLICATION ||
!isStructuralPublicationIndex(ev) ||
!addr ||
rowKey !== addr ||
!canon ||
canon.id !== ev.id
) {
rowKey !== addr
const superseded = Boolean(canon && canon.id !== ev?.id)
if (invalid || superseded) {
toDelete.push(rowKey)
}
cursor.continue()

Loading…
Cancel
Save