Users can now tap "Share → Readest" in Safari, Chrome, or any other
browser on iOS / Android and the article URL flows through the same
clip-and-import pipeline the in-app "From Web URL" entry uses.
Android
`MainActivity.handleIncomingIntent` already routed file shares via
`ACTION_SEND` + `EXTRA_STREAM`. Extend it to also pick up URL shares
via `ACTION_SEND` + `EXTRA_TEXT`: parse the first http(s) token out of
the text payload and dispatch it on the existing `shared-intent` event
channel. No new event channel needed — `useAppUrlIngress` already
listens and re-broadcasts as `app-incoming-url`.
The existing `<intent-filter>` for `ACTION_SEND` with `*/*` MIME type
already accepts `text/plain` from browsers — no manifest change
required.
iOS
`gen/apple/` gains a new ShareExtension target. The extension's
`ShareViewController` extracts a URL from `NSExtensionContext.inputItems`
(prefers `public.url`, falls back to first http(s) token in
`public.plain-text`) and forwards it to the main app as
`readest://clip?url=<encoded>` via the responder-chain `openURL:`
selector — the standard share-extension trick used by Pocket,
Instapaper, Matter, etc.
`project.yml` adds the ShareExtension target and switches the main app's
Info.plist / entitlements references to `INFOPLIST_FILE` /
`CODE_SIGN_ENTITLEMENTS` build settings instead of xcodegen's `info:` /
`entitlements:` blocks. That way the hand-tuned `Readest_iOS/Info.plist`
(CFBundleDocumentTypes, UTExportedTypeDeclarations, locales,
CFBundleURLTypes for readest://, applesignin, associated-domains for
Universal Links) is treated as an opaque input — xcodegen won't
regenerate it.
JS
New `useClipUrlIngress` hook subscribes to `app-incoming-url`,
unwraps `readest://clip?url=<encoded>` into the inner URL (the iOS
forwarding path), filters out file URIs and annotation deep links,
and runs each remaining http(s) URL through `clip_url` →
`convertToEpubWithWorker` → `ingestFile` — the same path `/send` uses.
Mounted alongside `useOpenWithBooks` and `useOpenAnnotationLink` in
both `app/library/page.tsx` and `app/reader/page.tsx` so shares
arriving while the user is reading still process.
Notes
- The PR targets `feat/send-clip-mobile` (PR #4252) since the share
pipeline depends on `clip_url` being available on mobile.
- iOS Share Extension built locally via xcodegen; the regenerated
pbxproj is tracked because gen/apple is gitignored.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a Share Book feature that generates an expiring HTTPS share URL plus a
parallel readest://share/{token} deep link. Recipients land on /s/{token},
where logged-in users can one-tap "Add to my library" (R2 server-side
byte-copy) and anonymous users download the book or open it in the app.
Sharers manage active links from a dedicated "Manage Shared Links" panel
under user settings.
Highlights:
- 9 new App Router endpoints under /api/share (create, [token], cover,
og.png, download, download/confirm, import, revoke, list).
- /s landing page with branded next/og chat unfurl image and SSR auth-cookie
detection so logged-in recipients see "Add to my library" as the primary
action without layout shift.
- Server-side R2 byte-copy for /import preserves the project's existing
invariant that every files.file_key is prefixed with its row's user_id;
stats / purge / delete / download routes work unchanged. URL-encodes the
copy source so titles with spaces or '&' don't break the copy.
- Universal 7-day expiry cap, no tier differentiation, no "never". DMCA-risk
reduction. Picker defaults to 3 days.
- Position-aware shares: "Share current page" toggle (off by default for
privacy) attaches the sharer's CFI; recipient lands at the same paragraph.
- Per-user 50-share cap, rate limiting via Cache-Control: no-store on
token-bearing responses, atomic SQL increment for download_count via a
SECURITY DEFINER function so the public confirm beacon stays safe under
concurrent fire.
- Soft revocation: presigned download URLs (5-min TTL) cannot be cancelled
before TTL; documented as accepted v1 behavior.
- token + token_hash hybrid storage: public endpoints look up by hash and
never select the raw token, so accidental SELECT-* leakage on a public
route can't expose the bearer credential.
- Mobile / desktop Tauri share via tauri-plugin-sharekit; web falls back to
navigator.share with a clipboard fallback when no native share method
exists. Share-sheet dismissal no longer silently copies.
UI:
- New Dialog with a settings-card group: iOS-style segmented duration picker
+ toggle slider for "Share current page", on a single row each.
- Reader top-bar Share button, library context-menu Share entry, manage-
shares list with cover thumbnails and overflow menu.
- New <SegmentedControl> primitive in src/components for reuse.
Coverage:
- Unit tests for token utils + URL parser (20 new tests, full suite at 3445).
- 31 locales translated for all new strings; en plurals hand-added per the
project's hand-curated en convention.
DB migration in docker/volumes/db/migrations/002_add_book_shares.sql adds
the book_shares table, RLS policies, and the increment_book_share_download
RPC. Migration is idempotent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(annotations): preview mode for deep-link landings
When the reader opens at a deep-link CFI (e.g. clicking an exported
highlight from Obsidian), the position should not be persisted as the
user's reading progress until they actually start reading. Otherwise
the deep-link visit overwrites their last-read position and propagates
that across all sync targets.
Adds a per-book `previewMode` flag in the reader store that:
- Is set to true in FoliateViewer when the URL's `?cfi=` overrides the
saved last-position.
- Is cleared on the first user-initiated relocate (page turn / scroll),
reusing the existing reason filter in `docRelocateHandler`.
- Gates the auto progress writers:
- useProgressAutoSave — skip local config persist
- useProgressSync — skip auto-push and skip the remote-progress
view.goTo (so cloud pull doesn't yank the
user away from the previewed annotation)
- useKOSync — skip auto-push (manual pushes still respected)
Hardcover sync and Discord presence are unaffected: hardcover only
fires on explicit user button press, and Discord presence carries no
position information.
Also picks up the regenerated AndroidManifest.xml change from the
existing tauri.conf.json deep-link config (registers readest:// scheme
on Android so the smart landing page's intent:// launch resolves).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(annotations): jump in place when target book is already open
When an annotation deep link arrives while the user is already in the
reader (most common case on mobile App Links), navigateToReader was
pushing the same /reader path with a different cfi query param. The
reader's init useEffect has [] deps, so it doesn't re-run, and
FoliateViewer doesn't re-read the cfi — the view stayed put.
Detect a mounted view for the target book hash by walking
viewStates and matching the hash prefix on the bookKey. If found,
call view.goTo(cfi) directly and set previewMode so the existing
gates fire. Falls back to navigateToReader when no view is open.
Also adds a console.log on each parsed deep link to make this path
easier to debug from device logs in the future.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(annotations): deep links for highlight exports
Embed an HTTPS deep link in markdown export so clicking a highlight in
Obsidian / Notion / Mail launches Readest at the exact CFI position.
Mobile App Links / Universal Links open the native app silently when
installed; desktop attempts the readest:// scheme automatically with a
manual fallback.
- Markdown export wraps the page-number text in a per-annotation link:
https://web.readest.com/o/book/{hash}/annotation/{id}?cfi=...
- New /o/... smart landing page handles platform routing (intent:// on
Android Chrome, scheme + visibility-cancel on other Android, auto
scheme + 1 s fallback on desktop, manual button on iOS).
- Reader honors a ?cfi= query param on initial load (overrides the
saved last-position for the primary book only).
- New useOpenAnnotationLink hook handles incoming readest:// and
https://web.readest.com/o/... URLs, including cold-start (getCurrent)
and library-load deferral; supports the legacy flat shape
readest://annotation/{hash}/{id} from previous Readwise syncs.
- ReadwiseClient now emits the HTTPS deep link instead of the legacy
custom scheme.
- AASA extended with /o/* matcher; Android intent-filter for the host
has no pathPrefix so it already covers it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n: translate annotation deep-link strings across all locales
Translates the 13 new keys introduced for the annotation deep-link
feature into all 31 supported locales. Replaces all 403
__STRING_NOT_TRANSLATED__ placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Apply alwaysOnTop setting on reader window init so books opened
in a new window correctly inherit the setting
- Update tauriHandleSetAlwaysOnTop to apply to all open windows
so toggling from any window keeps all windows in sync
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>