Add mobile resume-position restore with a compact Welcome back card#100
Conversation
Return the user to the exact body position where they previously stopped, gated by strict content validation so a convincing but incorrect jump is impossible. - Capture: a structural anchor (full top-level Muya block index including front matter, plus the viewport ratio inside that block) updates in memory on scroll settle and persists only through the existing document-exit and app lifecycle paths. - Store: a versioned, LRU-bounded localStorage store (marktext-for-android:resume-positions) keyed by stable document identity (local draft id / android-document source URI). Position writes never touch document, draft, or recent-document records, never dirty the Markdown, and never appear as saves. - Validation: SHA-256 of canonicalized Markdown plus a length fast check. Any mismatch or resolution failure silently discards the opportunity; displayText is presentation-only. - Restore: no automatic scrolling on open. A compact non-modal card below the top bar offers the jump; tapping scrolls instantly without focusing Muya or opening the keyboard, then runs a bounded ResizeObserver stabilization window (quiescence, hard cap, trusted user input, or generation cancellation) to re-pin the anchor through late layout shifts. The card dismisses on scroll, edit, competing editor surfaces, auto-timeout, or explicit close, and never takes Android Back priority. Verified: 410 unit tests (26 new), 96 e2e (9 new real-Muya resume specs), lint, typecheck, and an API 35 emulator smoke over CDP (capture on exit, card offer without auto-scroll, exact jump landing, focus/IME untouched, scroll dismissal).
…t settle Review round 1 fixes, one per finding: 1. Capture races and structurally stale anchors: persistNow() no longer depends on a debounced scroll-settle snapshot. It flushes pending editor content, then synchronously reads the live viewport anchor from the DOM at each persist point, so an exit inside any quiet window keeps the latest position and a post-edit exit can never pair a new hash with a pre-edit block index. The scroll listener now only tracks card dismissal and a positionTouched flag; a session without scrolling or editing still leaves the stored record untouched. 2. Latest-capture-wins persistence: capturedAt is stamped when the anchor is snapshotted (before hashing), and every write carries a per-document monotonic token checked after the digest resolves — an older SHA-256 completing late can never overwrite a newer capture, including across session resets. persistNow() returns the write's promise. 3. Bounded initial-layout settle before eligibility: startForOpenedDocument waits for .mu-container to stop resizing (350 ms quiet window, 3 s hard cap) before validating and measuring the target distance. A quiet window alone cannot see a network-stalled image (no resize events), so the settle also refuses to resolve while Muya marks an image probe as pending (mu-image-loading) or an <img> is incomplete. The offer is also skipped when the user scrolled or edited before evaluation finished. New regressions: unit — deterministic deferred-hash ordering (newer resolves first, older last; and across a session reset), live-capture-at-persist, and edit-only-session persistence; e2e — immediate exit inside the quiet window, second-scroll-wins on immediate exit, tall-block split without re-scroll landing at the exit position, and a delayed tall image above the target that must not suppress the card. Verified: 414 unit tests, 100 e2e, lint, typecheck, API 35 emulator smoke (8/8) plus a device race smoke: scrollTop=4200 and Back issued in the same frame still persists the latest anchor and the reopened jump lands at 4200.
|
All three findings addressed in 09f1234. 1. Quiet-window race + structurally stale anchors (merge-blocking) — capture no longer has a debounced snapshot to race against. 2. Unordered fire-and-forget writes — 3. Eligibility judged against transient layout — Verification: 414 unit (34 resume-related), 100 e2e (13 resume specs incl. the four new regressions: immediate exit in the quiet window, second-scroll-wins, tall-block split without re-scroll landing at the exit position, delayed-image eligibility), lint/typecheck/build, API 35 emulator smoke 8/8 ( |
…on removal 1. Post-jump stabilization now shares the initial settle's pending-image awareness: when its quiet window elapses while a Muya image probe (mu-image-loading) or an incomplete <img> is still in flight, the timer re-arms instead of disconnecting — a network wait produces no resize events, so quiet alone is not trustworthy. The eventual load's reflow re-pins the anchor through the observer; the 4 s hard cap and trusted user input still bound and cancel the window. 2. A hash-mismatch during opening-time validation no longer removes the store entry unconditionally. The removal is identity-checked against a re-read of the store (capturedAt + sha256): if a lifecycle capture landed a newer, valid record while the stale record's hash was still computing, the removal is skipped; if a newer write completes after the removal, it re-creates the entry — both orderings preserve the newest capture. New regressions: - e2e: an image slower than the initial-settle hard cap — the card is offered while the image is still pending, the first landing is measured ~3200 px short on the transient layout, and stabilization must re-pin to the captured position when the image lands (pre-fix, stabilization disconnected after 600 ms of quiet and left the user displaced). - unit: a stale validation mismatch racing a concurrent persistNow write — the newer record must survive regardless of completion order. Verified: 415 unit, 101 e2e, lint, typecheck, API 35 emulator smoke 8/8 plus the same-frame scroll+Back race smoke (mInputShown=false).
|
Both round-2 findings addressed in 9730f67. [P2] Stabilization dying under a resize-free network wait — the stabilization quiet handler now uses the exact same pending-awareness as the initial settle: when the 600 ms quiet window elapses while The new e2e covers the exact sequence from the finding: the image is served fast on the capture session (anchor measured on the full layout) and slower than the 3 s initial-settle cap on reopen, so the card becomes actionable while the image is still in flight. The test asserts the first landing is ~3200 px short on the transient layout, then that stabilization converges to the captured position once the image lands — pre-fix this fails exactly as described (stabilization disconnects after 600 ms of quiet, the user stays displaced). One debugging note worth recording: while building this test I initially concluded Muya leaves [P2] Stale validation deleting a newer record — mismatch removal is now identity-checked: before Verification: 415 unit, 101 e2e, lint/typecheck/build, API 35 emulator smoke 8/8 plus the same-frame scroll+Back race smoke ( |
What
A production "resume where you left off" flow for the mobile editor: the app remembers the exact body position where the user stopped reading or editing and, on the next open of the unchanged document, offers a compact non-modal "Welcome back" card below the top bar. Tapping the card jumps to the saved position; nothing ever scrolls automatically.
How
Capture (
features/editor/resumePosition.ts).mu-containerchildren map 1:1 ontomuya.getState()blocks) plus the viewport-top ratio inside that block.closeEditorToHome, lifecycle flush, document replacement, unmount). A visit that never scrolls leaves the stored position untouched.Persistence (
lib/resumePositions.ts)marktext-for-android:resume-positions, keyed by stable document identity (local-draft:{id}/android-document:{sourceUri}), LRU-bounded to 30 records by capture time; orphans age out through the cap.{ schemaVersion, capturedAt, normalizedLength, markdownSha256, topBlockIndex, topBlockRatio, displayText }. No cursor snapshots, DOM paths, runtime ids, or speculative fields.Validation
displayTextis presentation-only.Restore + stabilization
.mu-containerand the anchor block re-pins the target when displacement exceeds 8 px, stopping on layout quiescence (600 ms), hard cap (4 s), first trusted user input, editor destruction/replacement, or generation cancellation. Observers and listeners always disconnect; no permanent scroll-spy, no style/padding mutation.prefers-reduced-motion.Out of scope (by design): caret/selection restore, relocation after external edits, contextual text matching, cross-device sync, automatic navigation on open, permanent scroll tracking, Muya changes.
Verification
scrollTop0 before tap, jump landed at the captured offset, focus state unchanged andmInputShown=falseafter the jump, user scroll dismissed the card.Emulator screenshot shows the compact chip (icon + "Welcome back" + one-line ellipsized snippet + close) floating under the top bar without obscuring the editor.