Proposal: Read Mode¶
Status: Implemented (Phases 0–8; PDF support deferred to its own proposal) · Last updated: 2026-07-13
Summary¶
A local-first reader: import EPUBs, read with per-book display settings, highlight and annotate, chat with an AI companion about passages, and sync everything — encrypted to yourself — over nostr, with Blossom file backup and social reading (public shelves, browsing friends' libraries, foreign-highlight overlays).
Built by porting vibereader (the complete, live, two-profile-verified app — not a rough PoC) into PlebChat's shell, then swapping the renderer to foliate-js (the library Readest uses; epub.js is abandonware), then adopting two SvelteReader features. PlebChat supersedes vibereader, which freezes as reference; this repo now owns the protocol contract (Nostr Event Model).
Decisions (2026-07-13)¶
- Port first, swap renderer second. vibereader's code lands with its epub.js seam intact (proven, e2e-verifiable), then foliate-js replaces it as a contained step — the seam is two files (
epub/service.ts,epub/import.ts) and the Phase 3 e2e suite is the swap's acceptance gate. - SvelteReader adoptions: chapter-segmented progress bar; chat-threads-linked-to-annotations. Not adopted: client-side embeddings (different strategy planned); its PDF/files subsystem (prototype-grade — PDF support is a deferred phase with its own design).
- Protocol continuity: kinds 30101–30104 and the
vibereadersettings d-tag kept verbatim — both apps share the plebchat.me origin and relay data, so existing libraries appear on first sync. Accepted quirk: both live apps LWW the same 30103 settings event; a d-tag migration is documented as PlebChat's first owned protocol change (not built). - AI chat ports as-is (BYO endpoint, device-local threads,
// PAYMENTS:seam intact).
Architecture decisions¶
- Routing: vibereader's
ui.viewswitching (library | reader | ghost | browse) stays inside/read/+page.svelte— no subroute split (stores/reader lifecycle were built for single-page switching). Deep links via query params (/read?book=<sha>,/read?view=browse) mirrored withreplaceState. Vibereader's own header dissolves: Sync/Browse/Settings/Import become a mode toolbar under the shared TopBar; a new shell-levelimmersiveflag hides the TopBar in reader/ghost views. - Session & storage: mode-agnostic
$lib/session(cyphertap login → per-npub DB) with a start/stop hook registry; fresh DBplebchat::<npub>, one DB for all modes. Not inheritingvibereader::<npub>— two live apps sharing a DB with divergent version counters is a corruption vector; relay sync repopulates everything except book-file Blobs, and a one-time migration assistant (same origin — the old DB is reachable) offers to copybookFiles/coversBlobs across. - Theming translation (mechanical, at port time):
amber-500/600accents →primary· zinc borders →border·zinc-500/400text →muted-foreground· zinc hover fills →accent·zinc-50/900surfaces →background/card. Kept literal: the annotation highlight palette (user data, mirrors 30104 content) and reading themes inside the epub iframe (independent of app chrome theme by design). - Chat-on-annotations (Phase 8) inverts SvelteReader's design: optional
annotationIdon the device-localChatThreadrecord — not thread ids on the annotation, because annotation records mirror 30104 content 1:1 and chats never sync; SvelteReader's shape would be a silent protocol change.
Phases¶
- Phase 0 — Contract & docs: event model moved here with ownership header; this proposal rewritten; mkdocs nav.
- Phase 1 — Shared foundations: deps (
epubjs,idb,nanoid;jszipdev),$lib/db(plebchat::<npub>v1, eager open at login),$lib/session+ root-layout bridge,$lib/stores/settings(plebchat-settingskey), shellimmersivestore + TopBar hide. Verified: headlessncryptlogin smoke createsplebchat::<npub>. - Phase 2 — Reader core under
/read: epub service/import, read stores (sync/chat/foreign as interface stubs), library + reader components (theme-translated), view switch + toolbar +?book=deep links + immersive, CLAUDE.md gotchas batch 1. Verified: 9-step headless flow (import → render → ToC → highlight → deep-link → reload resume → annotation persistence). The jszip fixture builder works against epub.js — Phase 3 risk retired. - Phase 3 — Playwright suite (permanent acceptance harness + foliate-swap gate):
e2e/fixtures/epub.ts(jszip generated EPUB3, seed-parameterized),e2e/helpers.ts(ncryptlogin = fresh identity/DB per test, iframe text selection),e2e/read.test.ts— 14 tests: logged-out gate · import→card · sha256 dedup · two distinct books · open/paginate/ToC · reading-theme in iframe · progress resume + deep-link mirror · spine-sorted highlights across chapters · note add/edit · annotation delete · annotations survive reload · cascade delete · settings persist · immersive TopBar. All green twice locally (zero flakes); chat-panel test lands with Phase 4. - Phase 4 — AI chat:
$lib/ai/client.ts(verbatim from vibereader — SSE + single-shot fallback,// PAYMENTS:seam), reading-companion prompt, chat store, ChatPanel; chat toggle and "Chat about this" restored in toolbar/context menu. e2e: unconfigured-state pitch + full mocked-endpoint roundtrip (selection context → user message → assistant reply). Live SSE streaming not re-verified in the port — the client file is byte-identical to vibereader's battle-tested one; verify once against a real BYO endpoint at the Phase-5 manual gate. - Phase 5 — Sync + Blossom + sharing: nostr codecs/sync engine verbatim, Sync toolbar button + dirty dot, per-book Blossom backup/restore + availability probe, public-shelf toggle, annotation sharing (plaintext 30104 + optional NIP-84 9802), vibereader→plebchat migration assistant (full-DB copy, not just Blobs — covers never-synced vibereader users; offered via toast, e2e-tested), gotchas batch 2. Live two-profile gate executed headlessly against relay.abvstudio.net + nostr.download (2026-07-13), all steps green: alice imports/annotates/backs-up/syncs on profile 1 → clean profile 2 pulls library (ghost card), annotation count arrives, restores the file hash-verified from Blossom, opens with the annotation live, deletes + syncs → tombstone removes the book back on profile 1. Decision-3 acceptance proven: profile 2's first sync also pulled "Alice's Adventures in Wonderland" — a real vibereader-era book (with reading progress) appearing in PlebChat automatically.
- Phase 6 — Social reading: browse store/view (npub input + follows picker + readers-of-book), GhostView (read-only annotations without the file, restore-and-open), foreign-highlight overlays (explicit fetch, per-reader toggle, dashed underlines), "Find readers" in the info dialog,
?view=browsedeep link. Live two-identity gate executed (2026-07-13), all steps green: alice imports → backs up → shares book + annotation → syncs; bob browses her npub, sees the shelf + public annotation, downloads the book from her Blossom pointer (hash-verified), and toggles her highlights as an overlay in his own reader. - Phase 7 — foliate-js swap: vendored at commit
78914ae(src/lib/read/epub/vendor/foliate/, local patches in VENDORED.md — open shadow roots for Playwright, pdf.js stub in place of the 13 MB pdfjs, paginatorgoTowaits out its lock instead of dropping);service.ts/import.tsrewritten against the unchanged contract;epubjsdropped; import accepts.epub/.mobi/.azw(3)/.fb2/.fbz/.cbz; locations cache obsolete (foliate's relocate carriesfractionnatively —ensureLocationsis now a no-op). Two behavioral fixes surfaced by the gate: paginator lock-dropping (patched) and non-navigation relocates clearing the selection (deduped by CFI in service.ts). Gates green (2026-07-13): all 22 e2e tests pass with assertions unchanged (only the helpers' iframe lookup adapted to shadow DOM); live compat gate — the vibereader-era Wonderland book restored from Blossom, opened under foliate, resumed at 3% from an epub.js-generated progress CFI, and rendered 5 annotations created under epub.js in vibereader. - Phase 8 — Adoptions: chapter-segmented ProgressBar built directly on foliate's native
getSectionFractions()(clickable per-section segments with ToC-label tooltips + current-position marker — no locations math needed); chat-on-annotations via optionalannotationIdon the device-local ChatThread (inverted from SvelteReader's design — no protocol change; no DB version bump either: an optional field needs no new store/index). Discuss affordance on annotation cards (reopens the same thread), linked-thread indicator in the chat list; empty threads stay ephemeral, a first message persists the link. e2e: 24 tests green (segments render + navigate; link survives reload; same thread reopens). - Phase 8.5 — SvelteReader-style UI polish (user feedback 2026-07-13): ProgressBar moved to the bottom of the reader (below the text, SvelteReader placement) and restyled — meta line (% read + current section label, both removed from the toolbar) over a rounded chapter-segment track with a primary progress fill and a floating hover tooltip naming the chapter. Annotation cards: only the quote itself is a click target (blank card space is inert; hover shows the target with
bg-accent/50), card lifts on hover; the hover-revealed Discuss/Share buttons stay as in Phase 8. Sync: the library's RefreshCw button replaced by aSyncStatusButtoncloud icon (cloud / spinning / green check / red alert + dirty dot) whose popover explains what sync does, shows last-sync time and the last run's pushed/pulled/removed/restorable counts, an error box, and an explicit "Sync now" button (sync store gainedstatus/error/lastSummary). Book cards: "Edit metadata" in the "…" menu opensBookEditDialog(title/author/publisher/language/ISBN/description + cover replace, center-cropped 2:3 ≤512px JPEG; save bumpsupdatedAtso the next sync publishes it; cover stays device-local). e2e: 21 tests green (two new: metadata edit roundtrip, sync popover open/close). - Phase 8.6 — Per-book sync opt-out + friend demo data (user feedback 2026-07-13):
Book.localOnlydevice-local flag (stripped from 30101 drafts likelastOpenedAt, documented in the event model) — a local-only book and its progress/annotations are excluded from sync push AND pull, from the dirty check, and lose their share affordances (Info dialog + annotation sidebar); chosen via SvelteReader-style "Sync to your relays / Local only" radio cards in the edit-metadata dialog. Already-pushed events stay on relays until deletion (documented; revisit if a "retract from relays" action is wanted). Demo data:scripts/seed-test-profiles.shpublishes kind 0 + kind 3 (alice/bob/carol follow each other) ande2e/seed-friends.test.ts(skipped unlessSEED=1) drives the real app per account — imports a distinct Gutenberg book (P&P / Frankenstein / Time Machine), makes a highlight + a note with real CFIs, shares both, shares the book, backs the file up to nostr.download, syncs (pinned to relay.abvstudio.net). Relay purges after 60 days → re-run both when demo data vanishes. Verified live: plaintext 30101/30104 on the relay, all three blobs HEAD 200. - Phase 9 — PDF/files support: designed in pdf-files.md (PDFs as a Files collection, kind 30112, deliberately non-highlightable — which is what dissolves the CFI blocker); ships as Phase 6 of four-modes.md. SvelteReader's implementation stays reference-only.
- Proposed, not scheduled: cover sync (cover-sync.md) — inline thumbnail in 30101 vs Blossom pointer; hybrid recommended.
Each phase is one reviewable commit to master (Phase 2 may split into two), gated on pnpm check + pnpm build + full Playwright suite; pnpm build && pnpm preview smoke before Phases 2, 5, 7 (base path removed 2026-07-13 — the app serves at the plebchat.me root).
Manual verification checklist (not CI-testable)¶
Sync/Blossom/browse need the pubkey-whitelisted relay (relay.abvstudio.net) and a Blossom server — CI can't reach them, so these are verified manually per phase with the alice/bob test accounts (../my-projects/.test-accounts.json), matching vibereader's Phase D/E precedent:
- Phase 5: alice imports + annotates + syncs → bob-as-alice on a second profile pulls the library; tombstone propagation; Blossom backup → wipe → restore. Decision-3 acceptance: an existing vibereader identity logs into PlebChat → first sync populates the library; migration assistant recovers file Blobs.
- Phase 6: alice shares book + annotations → bob browses alice's shelf → downloads the book from her Blossom pointer (sha-verified) → foreign-highlight overlay toggles in bob's reader.
- Phase 7: annotations created pre-swap (incl. relay-synced ones) render correctly under foliate.
Reference material¶
- vibereader — the source of the port:
src/lib/{db,epub,stores,nostr,ai}/,src/routes/+page.svelte; itsCLAUDE.mdgotcha list migrates here as phases land;proposals.md#2 is the accepted foliate-js plan (risks, vendoring, CFI compatibility). - SvelteReader
frontend/src/lib/components/reader/ProgressBar.svelte+epubService.getChapterPositions(Phase 8); its annotation/chat-thread linkage (inverted here). - foliate-js: github.com/johnfactotum/foliate-js — read Readest as the reference consumer.
- Specs:
nips/84.md,nips/73.md,nips/44.md,nips/09.md; Blossom BUD-01/02/03/06/11 (standard-base64 auth quirk documented in the event model).
Open questions¶
- Settings d-tag migration (
vibereader→plebchat) timing — after vibereader usage ends. - Local ephemeral relay (e.g.
nak serve) in CI to make sync testable — revisit after Phase 5. - ~~PDF/files design (Phase 9) — needs its own proposal before any code.~~ Answered by pdf-files.md (2026-07-15).