Skip to content

Theorem v1.0.7

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Jul 11:43
· 350 commits to main since this release

Fixed

  • Production Tauri: book shows "1-2 lines and white" — Two interrelated fixes:

    1. Invalid iframe sandbox flag — The reader iframe had allow-clipboard-write in the sandbox attribute, which is not a valid sandbox token (it's a Permissions-Policy directive). WebKitGTK (Tauri's production WebView on Linux) rejects the entire sandbox attribute when it encounters an invalid flag, dropping the iframe into maximum sandboxing. This broke foliate-js page-positioning logic that relies on allow-same-origin to read iframe content dimensions and set up CSS columns. Reverted to allow-same-origin allow-scripts.

    2. CSP 'unsafe-inline' ignored due to Tauri style nonce injection — An inline <style> tag in index.html (loading spinner, added in v1.0.7) triggered Tauri's CSP processing pipeline: the build system injects __TAURI_STYLE_NONCE__ into <style> elements, and at runtime replaces it with a real nonce + adds 'nonce-xxx' to style-src. Per CSP spec, when a nonce source is present in a directive, 'unsafe-inline' is ignored — so all dynamic inline styles (including foliate-js's <style> elements inside the reader iframe that set column-width, height, padding) were blocked. The loading spinner CSS is now in src/index.css (loaded via <link> from 'self'), eliminating the inline <style> element and keeping 'unsafe-inline' intact.

    3. Tauri IPC blocked by CSP — The CSP connect-src was missing ipc: and http://ipc.localhost, causing Tauri's custom IPC protocol to fail and fall back to postMessage. Added ipc: http://ipc.localhost to connect-src.

  • Production builds must not ship with devtools — Removed devtools feature flag from tauri dependency in Cargo.toml.

Added

  • On-demand download progress — Reader shows a real progress bar with file size (e.g., 12.5 MB / 45.3 MB (28%)) instead of an indeterminate spinner when downloading synced books. Progress events emitted from Rust in 1MB chunks, throttled to percentage changes.
  • Memorized reader state — Reader now persists downloadingBookId and downloadProgress across renders. "Try Again" button on all error screens.

Changed

  • File transfer: streaming writes with no IPCdownload_book_file writes received data directly to book-cache/{id}.book from Rust in 1MB chunks. Zero bytes pass through the Tauri IPC bridge. Previously, the entire file was returned via IPC as JSON number[], causing 360MB+ allocations that crashed Android with OOM (heap limit 256MB).
  • File transfer: 120s timeouts — All I/O operations (connect, open_bi, read_line, read_exact) now have 120s timeouts. Previously, request_book_file could hang indefinitely on unreachable peers.
  • File transfer: SQLite fallbackFileTransferHandler falls back to reading from SQLite books table when the file is not in book-cache. Previously only served from book-cache, meaning locally-imported books (stored in SQLite) were unavailable to peers.
  • Sync: gossip-based bidirectional auto-syncNeighborUp events now properly trigger runDeviceSync on the peer via docs-peer-online. The iroh_node_id field was missing from PairedDeviceInfo, making the peer-matching d.irohNodeId === nodeId always fail. Auto-sync when a peer comes online was completely dead.
  • Sync: settle threshold fixedsignalSettle() checks >= 2 instead of >= 3. Only 2 events (docs-pending-content-ready + docs-sync-finished) ever call signalSettle, so the threshold of 3 was never reached — every sync round waited the full 30s timeout.
  • Sync: event subscription on doc recoverysubscribe_doc_events is now called after re-importing a sync doc from ticket in docs_sync_now. Previously, doc subscriptions were lost during recovery, causing no live events to flow.
  • Sync: _bridgePaused guard for all stores — Vocabulary, RSS, and settings store subscriptions now check _bridgePaused during provisioning, preventing writes during initial data sync.
  • Reader: shortcuts useEffect moved before early returns — The keyboard shortcuts registration useEffect is now called before all early-return guards (downloading, error, syncedWithoutFile). Previously, the shortcuts useEffect was duplicated — one copy before and one after the early returns — causing "Rendered more hooks than during the previous render" errors when transitioning from the download screen to the reader.
  • Reader: polling loop timeout — The while (syncedWithoutFile) polling loop now has a 120-second timeout. Previously it hung forever if the download failed.
  • Annotations page: fully responsive card view — Touch swipe navigation, icon-based mobile toolbars, clipped dot indicators, touch-manipulation on all interactive elements.

Performance

  • Memory: eliminate unnecessary Blob copy — When the MIME type already matches, use the blob directly instead of new Blob([blob], type), which duplicated the entire book in JS memory. Saves one full copy (50-100MB for large EPUBs).
  • Memory: call EPUB.destroy() on engine teardownFoliateEngine.destroy() now calls EPUB.destroy(), which revokes all blob URLs created for sections, CSS, images, and fonts. Previously, blob URLs accumulated until page refresh.
  • Memory: unload previous fixed-layout spreadsgoToSpread() now calls unload() on the previous spread's sections. Fixed-layout books (children's books, comics) were leaking all visited spreads' blob URLs.
  • Memory: cover extraction early-return before file re-read — The hasRealCover && coverExtractionDone check now runs BEFORE getBookData(), avoiding a full re-read of the book from storage when cover is already extracted.
  • Memory: metadata-only EPUB prefetch — Rust prefetch_zip_metadata no longer pre-decodes all EPUB section HTML files (up to 100 sections, ~3-6MB of JS strings). Only metadata files (container.xml, OPF, NAV, NCX, encryption) are prefetched. Sections load lazily via zip.js.
  • Binary size: removed unused Rust dependenciesread_zip_entry wrapper removed after section prefetch elimination.

Fixed (Android)

  • ndk_context restore — Re-added JNI initNdkContext and ndk-context dependency. iroh's netwatchnetdevndk_context::android_context() panics if the Android JVM/context are not initialized before any iroh networking operations run. Without this, the app crashed on Android 15 at startup.
  • ndk_context init after super.onCreateinitNdkContext(applicationContext) moved after super.onCreate() to ensure applicationContext is non-null when the JNI call fires.
  • CBR/RAR desktop-onlyunrar-ng (bundled UnRAR C++ source) uses lutimes() which was removed from Android's bionic libc in NDK 28. CBR conversion works on desktop; Android returns a clear "not supported" error to avoid compilation failure.
  • accept loop gate — When no paired devices exist, the accept loop registers only pairing + file_transfer handlers, skipping gossip/docs creation. This prevents the ndk_context::android_context() panic on fresh Android installs with no paired devices.
  • Top-bar single-device sync — Sync button now syncs only the first paired device instead of iterating all, preventing sequential timeouts on offline peers.
  • Mobile safe-area padding — All pages (Settings, Statistics, Bookmarks, Annotations, Feeds, Vocabulary, Shelves) now use pb-[calc(var(--spacing-2xl)+env(safe-area-inset-bottom))] to clear the bottom navigation on notched devices.

Fixed (Sync)

  • Duplicate pairing by iroh_node_id — The dedup check only matched by fingerprint, which is empty on Android (and often empty on desktop). Added iroh_node_id-based dedup in both handle_pair_req and submit_pairing_code.
  • Frontend duplicate device listDeviceSync.tsx filtered by deviceId before appending, preventing duplicate entries in the local React state.

Fixed (General)

  • Stuck "Downloading Book" screen — Added 120s timeout, clears downloadingBookId on timeout, shows "Book download timed out" error with retry.
  • Broken reader state after download failure — New guard renders "Book File Not Available" screen when syncedWithoutFile is true but not currently downloading, instead of showing a broken reader view.
  • "Rendered more hooks than during the previous render" — Removed duplicate shortcuts useEffect that was left after the move. When transitioning from "Downloading" screen (early return fired, 1 shortcuts hook) to reader (no early return, 2 shortcuts hooks), React detected the mismatch and threw.