Skip to content

feat(mobile): 新增閱讀頁劃線註記功能,修正選字手勢與標記顯示問題#6

Merged
Retsomm merged 2 commits into
mainfrom
dev
Jul 5, 2026
Merged

feat(mobile): 新增閱讀頁劃線註記功能,修正選字手勢與標記顯示問題#6
Retsomm merged 2 commits into
mainfrom
dev

Conversation

@Retsomm

@Retsomm Retsomm commented Jul 5, 2026

Copy link
Copy Markdown
Owner
  • 劃線/顏色/感想筆記/清單管理/匯出(Share.share)第一版實作
  • 新增「劃線模式」切換,暫停翻頁點擊區以避免攔截長按選字手勢
  • 補上網頁版已有的 annotations hooks.render 競態保險機制(verify+reinject)

Summary by CodeRabbit

  • New Features

    • 新增閱讀註記功能,支援標記、顏色切換、筆記編輯與刪除。
    • 註記清單可直接跳轉到對應內容,並支援多筆選取後匯出。
    • 新增註記模式切換,方便在閱讀與標記操作間切換。
  • Bug Fixes

    • 改善選取文字與註記顯示的互動,提升在裝置上的可見性與操作穩定性。
    • 重新整理書籍時會一併保留與載入註記資料。

- 劃線/顏色/感想筆記/清單管理/匯出(Share.share)第一版實作
- 新增「劃線模式」切換,暫停翻頁點擊區以避免攔截長按選字手勢
- 補上網頁版已有的 annotations hooks.render 競態保險機制(verify+reinject)

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
travel-in-time Ready Ready Preview, Comment Jul 5, 2026 10:49am

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Retsomm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 941d6a08-2dc3-4526-a81d-4f297399197a

📥 Commits

Reviewing files that changed from the base of the PR and between 167bf58 and 255831b.

📒 Files selected for processing (4)
  • mobile/app/reader/[id].tsx
  • mobile/lib/library.ts
  • mobile/lib/readerHtml.generated.ts
  • mobile/reader-web/index.ts
📝 Walkthrough

Walkthrough

This PR adds a full annotation (highlight/notes) feature spanning data persistence (AsyncStorage-backed Annotation records), messaging protocol extensions between RN and the epub.js WebView, underline rendering with verification/reinjection logic, RN reader screen state/handlers, a new ListPanel notes tab with export, a new SelectionBar component, shared highlight colors, and setup-guide progress notes.

Changes

Annotation Feature

Layer / File(s) Summary
Annotation data model, storage, and colors
mobile/lib/library.ts, mobile/lib/annotationColors.ts
Adds Annotation interface, loadAnnotations/saveAnnotations AsyncStorage helpers, annotation cleanup on book removal, and exported HIGHLIGHT_COLORS palette.
Reader messaging protocol extensions
mobile/lib/readerMessages.ts
Adds AnnotationMark type and new InboundMessage/OutboundMessage variants for annotations, selection, and debug logging.
WebView annotation rendering and selection handling
mobile/reader-web/index.ts
Implements annotation-mode toggling, underline mark creation/verification/reinjection, native selection clearing, and selection/touch event handling in epub.js rendition.
SelectionBar component
mobile/components/SelectionBar.tsx
New bottom-fixed component with selection/edit modes for highlighting, searching, copying, and deleting annotations.
RN reader screen annotation wiring
mobile/app/reader/[id].tsx
Adds annotation/selection state, loads/syncs annotations with WebView, handles incoming events, implements CRUD handlers, and adds UI controls (toggle, banner, SelectionBar wiring).
ListPanel notes tab UI and export
mobile/components/ListPanel.tsx
Replaces placeholder with full annotation list, multi-select, color picker, note editing, delete confirmation, and Share-based export grouped by chapter.
Setup guide progress notes
RN_SETUP_GUIDE.md
Updates task status and adds detailed implementation/diagnosis notes for the annotation feature.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ReaderScreen
  participant WebView
  participant AsyncStorage

  User->>WebView: select text in book
  WebView->>ReaderScreen: textSelected(cfi, text)
  ReaderScreen->>ReaderScreen: show SelectionBar
  User->>ReaderScreen: choose highlight color
  ReaderScreen->>WebView: setAnnotations(annotations)
  WebView->>WebView: applyAnnotations / verifyAnnotationsRendered
  ReaderScreen->>AsyncStorage: saveAnnotations(id, annotations)
  ReaderScreen->>ReaderScreen: syncAnnotationsToWebView / update ListPanel
Loading

Possibly related PRs

  • Retsomm/TravelInTime#1: Extends the same WebView load/message and tap-zone logic in mobile/reader-web/index.ts and mobile/app/reader/[id].tsx that this PR builds annotation mode and messaging on top of.
  • Retsomm/TravelInTime#4: Introduces the notes-tab placeholder in mobile/components/ListPanel.tsx that this PR replaces with full annotation UI and actions.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 標題準確概括了閱讀頁劃線註記功能與選字手勢、標記顯示修正,和這次變更內容一致。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mobile/lib/library.ts (1)

140-153: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Await the storage cleanup, or handle its rejection. updateMeta only accepts a synchronous callback, so AsyncStorage.multiRemove(...) is fire-and-forget here. If it rejects, the rejection is unhandled and annotationsKey(id) can survive a successful delete. Move the cleanup outside updateMeta so it can be awaited, or attach a .catch() here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/lib/library.ts` around lines 140 - 153, The storage cleanup in
removeBook is currently fire-and-forget inside the updateMeta callback, so any
AsyncStorage.multiRemove rejection is unhandled and some keys may remain. Move
the multiRemove call out of updateMeta so removeBook can await it, or add
explicit rejection handling with catch; keep the file/cover deletion and record
removal logic in removeBook/updateMeta, and use the existing progressKey,
settingsKey, bookmarksKey, and annotationsKey helpers to locate the cleanup
call.
🧹 Nitpick comments (3)
mobile/components/SelectionBar.tsx (1)

31-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Move new JSX styling out of inline style props.

This new component adds several inline style objects; please convert these to the project’s className styling approach. As per coding guidelines, **/*.tsx: React JSX 頁面/元件中應使用 className 寫法,並避免使用 inline style.

Also applies to: 45-45, 49-49, 56-56, 66-68, 74-76, 84-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/components/SelectionBar.tsx` around lines 31 - 42, The new JSX in
SelectionBar uses inline style objects in the component render, which should be
converted to the project’s className-based styling. Update the relevant elements
in SelectionBar.tsx to use className instead of style, and move the
layout/spacing/positioning values currently applied inline into the existing
styling system so the JSX stays consistent with the app’s tsx guidelines.

Source: Coding guidelines

mobile/components/ListPanel.tsx (1)

262-283: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Move the new notes UI styling out of inline style props.

The notes tab adds many inline style objects; please convert these new JSX nodes to the project’s className styling approach. As per coding guidelines, **/*.tsx: React JSX 頁面/元件中應使用 className 寫法,並避免使用 inline style.

Also applies to: 285-421

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/components/ListPanel.tsx` around lines 262 - 283, The new notes UI in
ListPanel should stop using inline style objects and follow the project’s
className-based styling convention. Update the JSX around the annotations
actions and related notes UI blocks in ListPanel to replace inline style props
with className equivalents, keeping the same layout and visual behavior. Use the
existing component structure in ListPanel and nearby notes-related nodes as the
reference points while moving all new styling out of JSX inline objects.

Source: Coding guidelines

mobile/app/reader/[id].tsx (1)

484-487: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Move new JSX styling out of inline style props.

The newly added annotation toggle and banner use inline style objects; please convert these to the project’s className styling approach. As per coding guidelines, **/*.tsx: React JSX 頁面/元件中應使用 className 寫法,並避免使用 inline style.

Also applies to: 535-540

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/reader/`[id].tsx around lines 484 - 487, The annotation toggle and
banner in the reader screen are using inline JSX style objects instead of the
project’s className approach. Update the relevant JSX in the reader component
(including the annotation toggle and banner sections) to use className-based
styling, and move any conditional styling currently handled in inline style
props into reusable className logic or existing style utilities. Refer to the
reader page component and the annotation-related JSX blocks to locate and
replace these inline styles.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mobile/app/reader/`[id].tsx:
- Around line 105-108: The reset path in the reader screen clears React Native
selection state but does not clear the WebView’s native selection, so keep both
in sync by routing these flows through a shared clear helper. Update the reader
screen logic around the selection reset handling in [id].tsx so the helper
clears local state and also invokes the WebView clearSelection action, and reuse
it from the reset/settings/list-panel paths that currently only call
setSelection(null) and related state setters.
- Around line 202-220: The reader event handlers are logging user-selected book
text outside development, which can expose private content. Update the logging
in the reader screen’s message handling path (the textSelected branch and the
other selected-text logging site in the same component) to either remove the
text snippet entirely or wrap it behind __DEV__ only. Keep non-sensitive event
logs like selectionCleared/annotationTapped if needed, but ensure no selected
content is emitted in production.
- Around line 376-403: The annotation update handlers in
handleChangeAnnotationColor, handleDeleteAnnotation, and
handleUpdateAnnotationNote call saveAnnotations without handling its async
failures, so the UI can appear saved even when persistence fails. Update these
handlers (and the related saveAnnotations(id, next) call in this block) to await
or catch the save result and handle rejection consistently, using the existing
annotation state/update flow in [id].tsx so failures do not get silently
ignored.

In `@mobile/reader-web/index.ts`:
- Around line 491-496: `verifyAnnotationsRendered` is checking the full
`renderedAnnotations` set against only the currently visible DOM, so annotations
from other pages/chapters are incorrectly reported missing and trigger
unnecessary `reinjectAllAnnotations` calls. Update the verification logic to
consider only annotations expected in the current rendered view (using the same
view/page/chapter context that drives rendering), and apply the same fix to the
duplicate logic around `renderAnnotations`/`verifyAnnotationsRendered` at the
other referenced block.
- Around line 498-528: Failures in addAnnotationMark are currently swallowed,
but applyAnnotations still records the annotation in renderedAnnotations, which
blocks future retries for marks that never rendered. Update the applyAnnotations
flow so only successfully added annotations are inserted into
renderedAnnotations, using addAnnotationMark’s success/failure outcome as the
gate. If underline registration throws or returns an invalid result, leave the
annotation out of the rendered set so later syncs can retry it. Apply the same
fix anywhere else in the same applyAnnotations block that marks annotations as
rendered after addAnnotationMark.
- Around line 294-299: The temporary debug bridge in debugLog is currently
always active and can leak touch telemetry and selected text/CFI data over the
RN bridge. Update debugLog in mobile/reader-web/index.ts to be gated behind an
explicit development-only flag or make it a no-op in production, and ensure the
later post({ type: 'debug', ... }) path is only reachable when that guard is
enabled. Keep the existing debugLog symbol so it is easy to locate and remove or
disable before release.

---

Outside diff comments:
In `@mobile/lib/library.ts`:
- Around line 140-153: The storage cleanup in removeBook is currently
fire-and-forget inside the updateMeta callback, so any AsyncStorage.multiRemove
rejection is unhandled and some keys may remain. Move the multiRemove call out
of updateMeta so removeBook can await it, or add explicit rejection handling
with catch; keep the file/cover deletion and record removal logic in
removeBook/updateMeta, and use the existing progressKey, settingsKey,
bookmarksKey, and annotationsKey helpers to locate the cleanup call.

---

Nitpick comments:
In `@mobile/app/reader/`[id].tsx:
- Around line 484-487: The annotation toggle and banner in the reader screen are
using inline JSX style objects instead of the project’s className approach.
Update the relevant JSX in the reader component (including the annotation toggle
and banner sections) to use className-based styling, and move any conditional
styling currently handled in inline style props into reusable className logic or
existing style utilities. Refer to the reader page component and the
annotation-related JSX blocks to locate and replace these inline styles.

In `@mobile/components/ListPanel.tsx`:
- Around line 262-283: The new notes UI in ListPanel should stop using inline
style objects and follow the project’s className-based styling convention.
Update the JSX around the annotations actions and related notes UI blocks in
ListPanel to replace inline style props with className equivalents, keeping the
same layout and visual behavior. Use the existing component structure in
ListPanel and nearby notes-related nodes as the reference points while moving
all new styling out of JSX inline objects.

In `@mobile/components/SelectionBar.tsx`:
- Around line 31-42: The new JSX in SelectionBar uses inline style objects in
the component render, which should be converted to the project’s className-based
styling. Update the relevant elements in SelectionBar.tsx to use className
instead of style, and move the layout/spacing/positioning values currently
applied inline into the existing styling system so the JSX stays consistent with
the app’s tsx guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc2bd079-b3af-4506-aba6-972afd366d29

📥 Commits

Reviewing files that changed from the base of the PR and between f2fd67f and 167bf58.

📒 Files selected for processing (9)
  • RN_SETUP_GUIDE.md
  • mobile/app/reader/[id].tsx
  • mobile/components/ListPanel.tsx
  • mobile/components/SelectionBar.tsx
  • mobile/lib/annotationColors.ts
  • mobile/lib/library.ts
  • mobile/lib/readerHtml.generated.ts
  • mobile/lib/readerMessages.ts
  • mobile/reader-web/index.ts

Comment thread mobile/app/reader/[id].tsx
Comment thread mobile/app/reader/[id].tsx
Comment thread mobile/app/reader/[id].tsx Outdated
Comment on lines +376 to +403
saveAnnotations(id, next);
syncAnnotationsToWebView(next);
setSelection(null);
webviewRef.current?.postMessage(JSON.stringify({ type: 'clearSelection' }));
};

const handleChangeAnnotationColor = (annotationId: string, color: string) => {
if (!id) return;
const next = annotations.map((a) => (a.id === annotationId ? { ...a, color } : a));
setAnnotations(next);
saveAnnotations(id, next);
syncAnnotationsToWebView(next);
};

const handleDeleteAnnotation = (annotationId: string) => {
if (!id) return;
const next = annotations.filter((a) => a.id !== annotationId);
setAnnotations(next);
saveAnnotations(id, next);
syncAnnotationsToWebView(next);
setEditingAnnotationId(null);
};

const handleUpdateAnnotationNote = (annotationId: string, note: string) => {
if (!id) return;
const next = annotations.map((a) => (a.id === annotationId ? { ...a, note: note.trim() || undefined } : a));
setAnnotations(next);
saveAnnotations(id, next);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Handle annotation persistence failures.

saveAnnotations is async, but create/change/delete/note updates ignore rejection. A failed AsyncStorage write leaves the UI showing saved annotations that will disappear after reload.

Proposed fix
-    saveAnnotations(id, next);
+    void saveAnnotations(id, next).catch((err) => {
+      console.error('[reader] saveAnnotations failed', err);
+      setErrorMessage('註記儲存失敗,請稍後再試');
+    });

Apply the same pattern to the other saveAnnotations(id, next) calls in this block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/reader/`[id].tsx around lines 376 - 403, The annotation update
handlers in handleChangeAnnotationColor, handleDeleteAnnotation, and
handleUpdateAnnotationNote call saveAnnotations without handling its async
failures, so the UI can appear saved even when persistence fails. Update these
handlers (and the related saveAnnotations(id, next) call in this block) to await
or catch the save result and handle rejection consistently, using the existing
annotation state/update flow in [id].tsx so failures do not get silently
ignored.

Comment thread mobile/reader-web/index.ts
Comment thread mobile/reader-web/index.ts
Comment thread mobile/reader-web/index.ts Outdated
@Retsomm Retsomm merged commit 2b002f1 into main Jul 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant