From a8a8e76d1eaa90ef60bc5feb74345e9a9bf4ffab Mon Sep 17 00:00:00 2001 From: Henry <48483883+hfellerhoff@users.noreply.github.com> Date: Tue, 5 May 2026 15:13:39 -0400 Subject: [PATCH] progress --- src/app/_app.read.$id.tsx | 24 +++++++++++++++---- .../e2e/self-hosted/article-progress.spec.ts | 8 ++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/app/_app.read.$id.tsx b/src/app/_app.read.$id.tsx index 07d25530..c55285f5 100644 --- a/src/app/_app.read.$id.tsx +++ b/src/app/_app.read.$id.tsx @@ -100,24 +100,38 @@ function ReadPage() { }, }); - // Restore progress on open — wait a frame so layout is complete - const hasRestoredRef = useRef(false); + // Disable browser scroll restoration so it doesn't fight with our + // programmatic restoration (especially on full page reloads). + useEffect(() => { + const original = history.scrollRestoration; + history.scrollRestoration = "manual"; + return () => { + history.scrollRestoration = original; + }; + }, []); + + // Restore progress on open — wait a frame so layout is complete. + // We track the last restored progress value so we can re-run when the + // stored progress changes (e.g. IDB hydrates with stale data first, then + // SSE updates to the real saved progress). + const restoredProgressRef = useRef(null); useEffect(() => { - if (hasRestoredRef.current) return; if (feedItem == null) return; const progress = feedItem.progress ?? 0; if (progress <= 0) { - hasRestoredRef.current = true; + restoredProgressRef.current = progress; return; } + if (restoredProgressRef.current === progress) return; + const elements = getElements(articleRef.current); if (elements.length === 0) return; const targetIndex = Math.min(progress, elements.length - 1); - hasRestoredRef.current = true; + restoredProgressRef.current = progress; requestAnimationFrame(() => { selectElement(elements, targetIndex, true); }); diff --git a/tests/e2e/self-hosted/article-progress.spec.ts b/tests/e2e/self-hosted/article-progress.spec.ts index 132df44d..03bb5d09 100644 --- a/tests/e2e/self-hosted/article-progress.spec.ts +++ b/tests/e2e/self-hosted/article-progress.spec.ts @@ -57,8 +57,10 @@ test.describe("article progress tracking", () => { await page.mouse.wheel(0, 300); await page.waitForTimeout(50); } - // Allow scroll event handlers and debounce (500ms) + buffer - await page.waitForTimeout(1000); + // Allow scroll event handlers and debounce (500ms) to fire, plus extra + // buffer for the RPC call and IDB write (throttled at 2000ms) to finish + // before we reload the page. + await page.waitForTimeout(3000); // Verify we scrolled const scrolledTop = await scrollContainer.evaluate((el) => el.scrollTop); @@ -89,7 +91,7 @@ test.describe("article progress tracking", () => { // Wait for progress restoration — the element with data-article-selected // appears once the restoration effect fires and selects the saved element. const restoredSelected = page.locator("[data-article-selected]"); - await expect(restoredSelected.first()).toBeVisible({ timeout: 10000 }); + await expect(restoredSelected.first()).toBeVisible({ timeout: 20000 }); // Wait for SSE processing to settle so the scroll position is stable. await page.waitForTimeout(2000);