[PR #2302] [MERGED] feat: add synchronized reading progress for bookmarks #2056

Closed
opened 2026-03-02 12:00:22 +03:00 by kerem · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/karakeep-app/karakeep/pull/2302
Author: @esimkowitz
Created: 12/25/2025
Status: Merged
Merged: 2/19/2026
Merged by: @MohamedBassem

Base: mainHead: evan/reading-progress


📝 Commits (10+)

📊 Changes

20 files changed (+4769 additions, -57 deletions)

View changed files

📝 apps/mobile/components/bookmarks/BookmarkHtmlHighlighterDom.tsx (+38 -9)
📝 apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx (+43 -1)
📝 apps/web/app/dashboard/preview/[bookmarkId]/page.tsx (+1 -1)
📝 apps/web/app/reader/[bookmarkId]/page.tsx (+1 -0)
📝 apps/web/components/dashboard/preview/BookmarkPreview.tsx (+1 -4)
📝 apps/web/components/dashboard/preview/LinkContentSection.tsx (+1 -1)
📝 apps/web/components/dashboard/preview/ReaderView.tsx (+66 -29)
apps/web/components/dashboard/preview/ReadingProgressBanner.tsx (+43 -0)
📝 apps/web/lib/i18n/locales/en/translation.json (+3 -0)
packages/db/drizzle/0080_user_reading_progress.sql (+15 -0)
packages/db/drizzle/meta/0080_snapshot.json (+3349 -0)
📝 packages/db/drizzle/meta/_journal.json (+7 -0)
📝 packages/db/schema.ts (+41 -0)
📝 packages/shared-react/components/BookmarkHtmlHighlighter.tsx (+28 -12)
packages/shared-react/components/ScrollProgressTracker.tsx (+229 -0)
packages/shared-react/hooks/reading-progress.ts (+146 -0)
packages/shared/utils/reading-progress-dom.test.ts (+56 -0)
packages/shared/utils/reading-progress-dom.ts (+334 -0)
📝 packages/trpc/routers/bookmarks.test.ts (+304 -0)
📝 packages/trpc/routers/bookmarks.ts (+63 -0)

📄 Description

Summary

Track and restore reading position in reader mode across web and mobile. Uses text character offsets with anchor text verification to find the first visible paragraph and scroll back to it on return.

Closes #1423

Note on unsupported views

This PR only covers the reader view for now.

Archived views

Adding support for archived views would require enabling JavaScript execution in the archive view iframes. While SingleFile, the new Karakeep extension, and the parsers all default to sanitizing JavaScript when saving archived HTML, there is still a chance someone archives something with malicious JS code in it. I didn't want to be the one to make this call, however I'm eager to figure out how we can add support for reading progress to the archived views while minimizing the risk to users.

Live webpages - mobile

Adding support for live webpages in the mobile app should be very easy. Like with the reader view on mobile, this is encapsulated in a mobile webview so we can just inject a contentScript and the logic should be pretty much the same.

Live webpages - web

For live webpage on the web, I think this would be a great feature for the Karakeep browser extension! It can host the same contentScript, with a lookup to see if a page is in your bookmarks. If so, it can autoscroll to your last known position!

Architectural Decisions

Three-Layer Code Organization: Core utilities (reading-progress-core.ts) contain platform-agnostic logic shared between web and mobile. Platform-specific modules (reading-progress-dom.ts, reading-progress-webview-src.ts) extend the core with environment-specific implementations. Adds a generator script for transpiling the core logic into stringified JavaScript to inject into the Mobile WebView (and into iframes in the future).

Dual-Strategy Position Restoration: Uses anchor text (~50 chars of paragraph content) as primary lookup, with character offset as fallback. This makes restoration resilient to minor content changes while still working when anchor text isn't available.

Text Normalization: Collapses whitespace to single spaces before calculating offsets, ensuring consistent character counting regardless of HTML formatting variations.

Content-Ready Signal with RAF Fallback: Parent components signal when content is ready for restoration via the contentReady prop. The hook attempts restoration immediately when signaled, then uses a single requestAnimationFrame as a fallback if the layout hasn't been calculated yet. This avoids polling while ensuring restoration works even when the scroll parent needs one more paint cycle to have scrollable height.

Persist on Unload: Only persists changes to the DB when the view is unloaded or blurred (or when the app closes).

Separate Reading Progress Table: Reading progress is stored in a dedicated userReadingProgress table rather than as columns on the bookmarks table. This allows each user to track their own reading progress independently when viewing shared bookmarks.

Platform-Specific Scrolling: Web version handles nested scrolling (including Radix ScrollArea detection), while mobile assumes window-level scrolling only.

Testing Steps

Web - Full-Screen Reader

  1. Open a long article bookmark
  2. Click "Open full page" to enter reader mode
  3. Scroll partway through the article
  4. Close the tab or navigate away
  5. Re-open the same bookmark in reader mode
  6. Verify it scrolls to approximately where you left off

Web - Preview Panel

  1. Open sidebar preview for a long article
  2. Scroll partway through in the cached content view
  3. Close the preview
  4. Re-open the same bookmark's preview
  5. Verify position is restored

Mobile

  1. Open a long article in mobile reader view
  2. Scroll partway through
  3. Background the app (switch to another app)
  4. Return to Karakeep
  5. Verify position is maintained

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/karakeep-app/karakeep/pull/2302 **Author:** [@esimkowitz](https://github.com/esimkowitz) **Created:** 12/25/2025 **Status:** ✅ Merged **Merged:** 2/19/2026 **Merged by:** [@MohamedBassem](https://github.com/MohamedBassem) **Base:** `main` ← **Head:** `evan/reading-progress` --- ### 📝 Commits (10+) - [`38f8580`](https://github.com/karakeep-app/karakeep/commit/38f8580fc861b75380b2eb936d729fad75ffb58c) feat: add synchronized reading progress for bookmarks - [`a546c28`](https://github.com/karakeep-app/karakeep/commit/a546c28eed0b38bfd0f2b67fab4e80471dc757be) it works - [`587c5e9`](https://github.com/karakeep-app/karakeep/commit/587c5e9f201287c5aa7b8d6ce89b142c3f0d943b) align mobile functions - [`0abad35`](https://github.com/karakeep-app/karakeep/commit/0abad3507ebecea9b5796dc0358e0cbab7f09001) fmt - [`e41d15e`](https://github.com/karakeep-app/karakeep/commit/e41d15edb4e70614ab806795bd6ef3917e20660e) use generator for webview js fns, remove empty index.ts - [`63d9639`](https://github.com/karakeep-app/karakeep/commit/63d9639dad620ac76a0b0e20133356b645a0f3a7) revert comments - [`e65c6d2`](https://github.com/karakeep-app/karakeep/commit/e65c6d2be5dd24cc71ce5be0134ee3f4b899f877) move shared fns into core - [`aced52c`](https://github.com/karakeep-app/karakeep/commit/aced52cb74c87bd56b8ce93de98cee98ea866c47) address pr review - [`9e7c901`](https://github.com/karakeep-app/karakeep/commit/9e7c901c8cbe479d29c66094d4d87b5300357eb9) watch core too - [`cc0945d`](https://github.com/karakeep-app/karakeep/commit/cc0945db1b5ec8fb903becfde9517ae5a324703e) address some pr comments ### 📊 Changes **20 files changed** (+4769 additions, -57 deletions) <details> <summary>View changed files</summary> 📝 `apps/mobile/components/bookmarks/BookmarkHtmlHighlighterDom.tsx` (+38 -9) 📝 `apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx` (+43 -1) 📝 `apps/web/app/dashboard/preview/[bookmarkId]/page.tsx` (+1 -1) 📝 `apps/web/app/reader/[bookmarkId]/page.tsx` (+1 -0) 📝 `apps/web/components/dashboard/preview/BookmarkPreview.tsx` (+1 -4) 📝 `apps/web/components/dashboard/preview/LinkContentSection.tsx` (+1 -1) 📝 `apps/web/components/dashboard/preview/ReaderView.tsx` (+66 -29) ➕ `apps/web/components/dashboard/preview/ReadingProgressBanner.tsx` (+43 -0) 📝 `apps/web/lib/i18n/locales/en/translation.json` (+3 -0) ➕ `packages/db/drizzle/0080_user_reading_progress.sql` (+15 -0) ➕ `packages/db/drizzle/meta/0080_snapshot.json` (+3349 -0) 📝 `packages/db/drizzle/meta/_journal.json` (+7 -0) 📝 `packages/db/schema.ts` (+41 -0) 📝 `packages/shared-react/components/BookmarkHtmlHighlighter.tsx` (+28 -12) ➕ `packages/shared-react/components/ScrollProgressTracker.tsx` (+229 -0) ➕ `packages/shared-react/hooks/reading-progress.ts` (+146 -0) ➕ `packages/shared/utils/reading-progress-dom.test.ts` (+56 -0) ➕ `packages/shared/utils/reading-progress-dom.ts` (+334 -0) 📝 `packages/trpc/routers/bookmarks.test.ts` (+304 -0) 📝 `packages/trpc/routers/bookmarks.ts` (+63 -0) </details> ### 📄 Description ## Summary Track and restore reading position in reader mode across web and mobile. Uses text character offsets with anchor text verification to find the first visible paragraph and scroll back to it on return. Closes #1423 ## Note on unsupported views This PR only covers the reader view for now. ### Archived views Adding support for archived views would require enabling JavaScript execution in the archive view iframes. While SingleFile, the new Karakeep extension, and the parsers all default to sanitizing JavaScript when saving archived HTML, there is still a chance someone archives something with malicious JS code in it. I didn't want to be the one to make this call, however I'm eager to figure out how we can add support for reading progress to the archived views while minimizing the risk to users. ### Live webpages - mobile Adding support for live webpages in the mobile app should be very easy. Like with the reader view on mobile, this is encapsulated in a mobile webview so we can just inject a contentScript and the logic should be pretty much the same. ### Live webpages - web For live webpage on the web, I think this would be a great feature for the Karakeep browser extension! It can host the same contentScript, with a lookup to see if a page is in your bookmarks. If so, it can autoscroll to your last known position! ## Architectural Decisions **Three-Layer Code Organization**: Core utilities (`reading-progress-core.ts`) contain platform-agnostic logic shared between web and mobile. Platform-specific modules (`reading-progress-dom.ts`, `reading-progress-webview-src.ts`) extend the core with environment-specific implementations. Adds a generator script for transpiling the core logic into stringified JavaScript to inject into the Mobile WebView (and into iframes in the future). **Dual-Strategy Position Restoration**: Uses anchor text (~50 chars of paragraph content) as primary lookup, with character offset as fallback. This makes restoration resilient to minor content changes while still working when anchor text isn't available. **Text Normalization**: Collapses whitespace to single spaces before calculating offsets, ensuring consistent character counting regardless of HTML formatting variations. **Content-Ready Signal with RAF Fallback**: Parent components signal when content is ready for restoration via the `contentReady` prop. The hook attempts restoration immediately when signaled, then uses a single `requestAnimationFrame` as a fallback if the layout hasn't been calculated yet. This avoids polling while ensuring restoration works even when the scroll parent needs one more paint cycle to have scrollable height. **Persist on Unload**: Only persists changes to the DB when the view is unloaded or blurred (or when the app closes). **Separate Reading Progress Table**: Reading progress is stored in a dedicated `userReadingProgress` table rather than as columns on the bookmarks table. This allows each user to track their own reading progress independently when viewing shared bookmarks. **Platform-Specific Scrolling**: Web version handles nested scrolling (including Radix ScrollArea detection), while mobile assumes window-level scrolling only. ## Testing Steps ### Web - Full-Screen Reader 1. Open a long article bookmark 2. Click "Open full page" to enter reader mode 3. Scroll partway through the article 4. Close the tab or navigate away 5. Re-open the same bookmark in reader mode 6. Verify it scrolls to approximately where you left off ### Web - Preview Panel 1. Open sidebar preview for a long article 2. Scroll partway through in the cached content view 3. Close the preview 4. Re-open the same bookmark's preview 5. Verify position is restored ### Mobile 1. Open a long article in mobile reader view 2. Scroll partway through 3. Background the app (switch to another app) 4. Return to Karakeep 5. Verify position is maintained --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
kerem 2026-03-02 12:00:22 +03:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/karakeep#2056
No description provided.