Skip to content

feat(mobile): auto-hiding reader toolbar with customizable actions#2527

Open
esimkowitz wants to merge 12 commits intokarakeep-app:mainfrom
esimkowitz:mobile-reader-action-bar
Open

feat(mobile): auto-hiding reader toolbar with customizable actions#2527
esimkowitz wants to merge 12 commits intokarakeep-app:mainfrom
esimkowitz:mobile-reader-action-bar

Conversation

@esimkowitz
Copy link
Copy Markdown
Contributor

@esimkowitz esimkowitz commented Feb 27, 2026

Resolves #2261
Resolves #2262
Resolves #2263

Summary

Auto-hiding reader bars (#2263)

When scrolling down in the bookmark reader, the top navigation bar and bottom toolbar now automatically hide to maximize reading space. Scrolling back up reveals them again. This applies to all reader view types (browser, reader, archive, and text).

  • New useScrollDirection hook detects scroll direction and toggles bar visibility
  • Header hides via navigation.setOptions({ headerShown }), footer animates out via react-native-reanimated translateY
  • Progress bar in reader view hides alongside the bars
  • Scroll offset callbacks threaded through all bookmark view components (BookmarkLinkView, BookmarkTextView, and all link preview variants)

Note — Android / iOS ≤ 18: On these platforms, only the bottom toolbar hides on scroll; the top navigation bar and reading progress bar remain visible. This is intentional. The opaque navigation header on older platforms participates in layout flow — toggling headerShown triggers a layout reflow that causes the absolutely-positioned footer to re-measure via onLayout, producing a visible flicker in the toolbar icons during the animation. On iOS 26, the header is transparent (headerTransparent: true) and doesn't affect layout, so both bars can safely toggle together.

Customizable bottom toolbar (#2261)

The bottom action bar is now fully customizable. Users can choose which actions appear directly on the toolbar (up to 6) and which go into an overflow menu accessible via an ellipsis button.

  • New actions: favourite (with gold fill toggle), archive, and share (supports links, text clipboard copy, image/PDF file sharing)
  • Overflow menu: native MenuView with SF Symbol icons, plus an "Edit Toolbar..." shortcut at the bottom
  • Toolbar settings page: accessible from Settings > Toolbar Buttons or from the overflow menu. Drag-to-reorder within each section, +/- buttons to move actions between visible and overflow. Reset to Defaults button.
  • Defaults: lists, tags, info, favourite, share, browser visible on the bar; archive and delete in the overflow menu

Redesigned "Continue reading" banner

Shortened from "Continue where you left off" to "Continue reading", made the banner a pill shape where the whole banner is a touch target. Renders as Liquid Glass on iOS 26. Uses expo-blur to apply a blur effect on iOS <26.

iOS 26 polish

  • Glass pill toolbar container via expo-glass-effect when available on iOS 26
  • Transparent headers with edge-to-edge content
  • Content insets for scrollable views to avoid header/footer overlap

Test plan

  • Scroll down in reader view — header and toolbar hide; scroll up — they reappear
  • Tap ellipsis — overflow actions appear with SF Symbol icons + "Edit Toolbar..."
  • Settings > Toolbar Buttons — drag to reorder, use +/- to move between visible and overflow
  • Remove all visible actions — only ellipsis shows on toolbar
  • Reset to Defaults restores original layout
  • Favourite/archive/share work from both toolbar and overflow menu
  • iOS 26: glass pill styling, transparent header, no extra padding below pill
  • Pre-iOS 26 / Android: solid header background, standard toolbar layout, only bottom toolbar auto-hides
  • Android: overflow menu items are flat (no nested submenu)
  • Share action: test that sharing works on Android and iOS for all supported bookmark types

…s and overflow menu

Redesign the bookmark reader bottom bar with configurable toolbar actions,
an overflow ellipsis menu using native MenuView, and auto-hide on scroll.
Add toolbar settings page for reordering visible/overflow actions. Support
iOS 26 glass pill styling and transparent headers with edge-to-edge content.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a configurable toolbar: new Toolbar Buttons settings page and settings fields; implements a data-driven toolbar registry and BottomActions refactor; threads scroll-offset and barsVisible through bookmark previews; adds useScrollDirection hook, iOS26 header/footer handling, and animated footer/banner behaviors.

Changes

Cohort / File(s) Summary
Toolbar Settings UI
apps/mobile/app/dashboard/settings/toolbar-settings.tsx
New draggable page to reorder/promote/demote toolbar and overflow actions, persist to settings, and reset to defaults.
Settings & Defaults
apps/mobile/lib/settings.ts
Added ToolbarActionId type, DEFAULT_TOOLBAR_ACTIONS, DEFAULT_OVERFLOW_ACTIONS, extended settings schema with toolbarActions and overflowActions, and load-time reconciliation of missing IDs.
Navigation & Layout
apps/mobile/app/dashboard/(tabs)/(settings)/index.tsx, apps/mobile/app/dashboard/_layout.tsx
Added "Toolbar Buttons" navigation entry and route; added iOS26 detection and conditional header blur/transparent behavior.
Bookmark Page Integration
apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx
Integrated useScrollDirection to toggle native header, added animated footer translation and footer-height capture, edge-to-edge handling for iOS26, and forwarded scroll callbacks to nested views.
Scroll Direction Hook
apps/mobile/lib/useScrollDirection.ts
New hook exposing barsVisible and onScrollOffsetChange(y) to show/hide bars based on scroll direction thresholds.
Bookmark Preview & Highlighter
apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx, apps/mobile/components/bookmarks/BookmarkHtmlHighlighterDom.tsx
Threaded onScrollOffsetChange/barsVisible through previews; added animated reader banner, contentInsetTop handling; added showProgressBar? and background/minHeight handling in highlighter.
Bookmark View & Text View Props
apps/mobile/components/bookmarks/BookmarkLinkView.tsx, apps/mobile/components/bookmarks/BookmarkTextView.tsx
Extended props to include onScrollOffsetChange, barsVisible, and contentInsetTop; wired ScrollView/WebView insets, contentOffset, and onScroll forwarding.
Toolbar Actions Registry & BottomActions
apps/mobile/components/bookmarks/BottomActions.tsx
Introduced exported TOOLBAR_ACTION_REGISTRY, centralized metadata-driven actions, menu/overflow integration, sharing/download flows, haptics/toasts, and glass-pill UI handling.
Draggable List & Glass Dep
apps/mobile/package.json
Added react-native-draggable-flatlist and expo-glass-effect dependencies.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(mobile): auto-hiding reader toolbar with customizable actions' clearly and concisely summarizes the main changes: auto-hiding toolbar and customization features.
Linked Issues check ✅ Passed The PR fulfills both linked issues: implements auto-hiding bars via useScrollDirection hook and navigation control (#2263), and provides customizable toolbar with drag-to-reorder, overflow menu, and settings page (#2261).
Out of Scope Changes check ✅ Passed All code changes directly support the two feature objectives: scroll tracking, bar visibility control, toolbar customization UI, action handlers, and platform-specific styling enhancements.
Description check ✅ Passed The PR description clearly outlines the implementation of auto-hiding reader bars and customizable toolbar features with detailed technical explanations and test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 27, 2026

Greptile Summary

Implements auto-hiding reader toolbar with customizable actions, iOS 26 glass pill styling, and overflow menu with SF Symbol icons.

Key Changes:

  • Auto-hiding header/footer based on scroll direction with smooth animations
  • Drag-and-drop toolbar customization (max 6 visible actions)
  • Native context menu for overflow actions with "Edit Toolbar..." shortcut
  • New actions: favourite, archive, and share (supports links, text clipboard, and asset file sharing)
  • iOS 26-specific features: glass pill toolbar, transparent headers, edge-to-edge content

Issues Found:

  • Critical logic bug in favourite toggle that will cause incorrect behavior with rapid clicks (uses stale bookmark state instead of optimistic state)
  • Minor style issue with variable naming convention (useGlassPill not a hook)

Confidence Score: 3/5

  • This PR has one critical logic bug that needs fixing before merge
  • The favourite toggle bug will cause incorrect behavior when users click multiple times quickly, as it uses stale bookmark state instead of the optimistic state. The rest of the implementation is well-structured with proper error handling and consistent patterns.
  • Pay close attention to apps/mobile/components/bookmarks/BottomActions.tsx line 323-324 for the favourite toggle bug

Important Files Changed

Filename Overview
apps/mobile/lib/useScrollDirection.ts New hook for scroll direction tracking with threshold-based hide/show logic
apps/mobile/app/dashboard/settings/toolbar-settings.tsx New drag-and-drop toolbar configuration page with promote/demote actions
apps/mobile/components/bookmarks/BottomActions.tsx Major refactor: added action registry, overflow menu, glass pill styling, share functionality - has favourite toggle bug with rapid clicks
apps/mobile/lib/settings.ts Added toolbar action types and default configurations to settings schema
apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx Implemented auto-hiding header/footer, animated footer, iOS 26 transparent header support

Last reviewed commit: 497d21f

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

13 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
apps/mobile/app/dashboard/settings/toolbar-settings.tsx (2)

25-34: Potential stale closure in save callback.

The save callback captures settings in its closure and spreads it when saving. If settings changes between renders but before save is called, you may inadvertently overwrite other settings that changed concurrently.

Consider only updating the specific fields:

♻️ Suggested refactor
   const save = useCallback(
     (nextVisible: ToolbarActionId[], nextOverflow: ToolbarActionId[]) => {
-      setSettings({
-        ...settings,
+      setSettings((prev) => ({
+        ...prev,
         toolbarActions: nextVisible,
         overflowActions: nextOverflow,
-      });
+      }));
     },
-    [settings, setSettings],
+    [setSettings],
   );

This requires setSettings to support a functional update pattern. If it doesn't currently, the existing approach is acceptable but should be noted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/app/dashboard/settings/toolbar-settings.tsx` around lines 25 -
34, The save callback currently closes over settings and spreads it when calling
setSettings, which can cause stale-overwrite; change save to use the functional
updater form of setSettings so you only update toolbarActions and
overflowActions based on the previous state (e.g., call setSettings(prev => ({
...prev, toolbarActions: nextVisible, overflowActions: nextOverflow }))). Ensure
setSettings supports a functional update; if not, note the limitation or convert
the setter to accept a functional updater.

74-77: Consider adding accessibility labels for drag handles.

The long-press interaction for dragging is not accessible to screen reader users. Consider adding accessibility hints to indicate the drag functionality:

 <TouchableOpacity
   activeOpacity={0.7}
   onLongPress={drag}
   disabled={isActive}
   className="flex flex-row items-center gap-3 bg-card px-4 py-3"
+  accessibilityLabel={`${meta.label}, drag to reorder`}
+  accessibilityHint="Long press and drag to reorder"
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/app/dashboard/settings/toolbar-settings.tsx` around lines 74 -
77, The TouchableOpacity acting as the drag handle (the component with
onLongPress={drag} and disabled={isActive}) needs accessibility props so screen
readers know it supports reordering; add accessible={true}, an accessibilityRole
(e.g., "button" or "adjustable"), an accessibilityLabel like "Reorder item" and
an accessibilityHint such as "Long press and drag to reorder this item", and
ensure the disabled state updates accessibilityState accordingly so
VoiceOver/TalkBack convey when dragging is unavailable.
apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx (2)

98-102: Consider extracting the magic number 44.

The value 44 represents the standard iOS navigation bar height. While this is a well-known constant, consider extracting it for clarity:

const IOS_NAV_BAR_HEIGHT = 44;
const contentInsetTop = isIOS26 ? insets.top + IOS_NAV_BAR_HEIGHT : 0;

This same value appears on line 130 in BookmarkLinkPreview.tsx, suggesting a shared constant would improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/app/dashboard/bookmarks/`[slug]/index.tsx around lines 98 - 102,
Extract the magic number 44 into a clearly named constant (e.g.,
IOS_NAV_BAR_HEIGHT) and use it when computing contentInsetTop (where isIOS26 is
used) so the line const contentInsetTop = isIOS26 ? insets.top + 44 : 0 becomes
insets.top + IOS_NAV_BAR_HEIGHT when applicable; also replace the same literal
in BookmarkLinkPreview (the other occurrence) or move the constant to a shared
module and import it to keep a single source of truth.

29-30: Platform.Version type handling may be inconsistent.

On iOS, Platform.Version is typically a string (e.g., "26.0"), but the type definition allows both string and number. The parseInt call handles strings correctly, but the as string cast could mask type issues if React Native's behavior changes.

This works correctly for iOS, but consider using a more defensive approach:

 const isIOS26 =
-  Platform.OS === "ios" && parseInt(Platform.Version as string, 10) >= 26;
+  Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/app/dashboard/bookmarks/`[slug]/index.tsx around lines 29 - 30,
The isIOS26 check uses a forced cast (Platform.Version as string) which can mask
runtime types; update the check to defensively handle both string and number
values for Platform.Version (e.g., use typeof Platform.Version === "number" ?
Platform.Version : parseInt(String(Platform.Version), 10) or Number/Math.floor
on the string) so isIOS26 correctly evaluates on iOS regardless of whether
Platform.Version is a number or a string; replace the current isIOS26 assignment
with this type-guarded conversion referencing Platform.Version and isIOS26.
apps/mobile/components/bookmarks/BottomActions.tsx (2)

36-37: Duplicate isIOS26 constant across files.

The isIOS26 check is duplicated in apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx (line 29-30) and here. Consider extracting this to a shared utility:

// apps/mobile/lib/platform.ts
export const isIOS26 = Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/components/bookmarks/BottomActions.tsx` around lines 36 - 37, The
isIOS26 platform check is duplicated; create a shared constant (e.g., export
const isIOS26 in a new apps/mobile/lib/platform.ts) implementing Platform.OS ===
"ios" && parseInt(String(Platform.Version), 10) >= 26, then replace the local
const isIOS26 in BottomActions.tsx (and the duplicate in
dashboard/bookmarks/[slug]/index.tsx) with an import from that shared utility
and remove the duplicated declaration so both files use the single exported
isIOS26.

203-227: Hardcoded file extension may cause issues with non-JPEG images.

The temp file is always saved as temp_image.jpg regardless of the actual image format (PNG, WebP, etc.). This could cause issues with sharing certain image types.

Consider deriving the extension from the asset metadata or content type:

♻️ Suggested improvement
         if (
           bookmark.content.assetType === "image" &&
           (await Sharing.isAvailableAsync())
         ) {
           const assetUrl = `${settings.address}/api/assets/${bookmark.content.assetId}`;
-          const fileUri = `${FileSystem.documentDirectory}temp_image.jpg`;
+          const fileName = bookmark.content.fileName || "temp_image";
+          const fileUri = `${FileSystem.documentDirectory}${fileName}`;
           const downloadResult = await FileSystem.downloadAsync(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/components/bookmarks/BottomActions.tsx` around lines 203 - 227,
The code saves images to a hardcoded temp filename ("temp_image.jpg") which
breaks for non-JPEGs; change the logic in the BookmarkTypes.ASSET branch (around
fileUri, FileSystem.downloadAsync and subsequent delete) to derive the file
extension from the asset metadata or response content-type before creating
fileUri: use bookmark.content.fileName or assetId metadata if available,
otherwise perform a lightweight HEAD/fetch to read the Content-Type header and
map it to an extension (e.g., image/png -> .png) with a safe fallback like .jpg;
then construct fileUri using that extension, call FileSystem.downloadAsync into
that path, and keep the existing shareAsync and deleteAsync flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/mobile/components/bookmarks/BottomActions.tsx`:
- Around line 320-327: The onClick handler is using the stale
bookmark.favourited value instead of the optimistic isFavourited state, causing
incorrect toggles on rapid clicks; update the onClick in BottomActions (the
handler that calls Haptics.selectionAsync and favouriteBookmark) to compute the
new value from isFavourited (i.e. pass favourited: !isFavourited to
favouriteBookmark) so the optimistic state drives the network call and UI
remains consistent with rapid toggles.
- Line 380: The constant name useGlassPill wrongly suggests a React hook; rename
the symbol (e.g., to isGlassPill or glassPillEnabled) and update all references
in this module to the new name (replace usages of useGlassPill in
JSX/conditionals such as the places currently checking it). Keep the
initialization logic the same: const isGlassPill = isIOS26 &&
isGlassEffectAPIAvailable(); and update any imports/exports or local references
(including the two occurrences in this file) to use the new identifier.

In `@apps/mobile/package.json`:
- Line 40: The project currently pulls react-native-reanimated@~4.1.1 via
react-native-draggable-flatlist@4.0.3 which requires the React Native New
Architecture but the repo config does not enable it; either enable New
Architecture flags in your app configuration (set newArchitecture: true for both
ios and android in app.config.js and ensure build profiles in eas.json account
for it) so reanimated v4 can run, or downgrade react-native-reanimated to a 3.x
compatible version and update package.json and lockfile accordingly to remove
the v4 dependency from react-native-draggable-flatlist resolution.

---

Nitpick comments:
In `@apps/mobile/app/dashboard/bookmarks/`[slug]/index.tsx:
- Around line 98-102: Extract the magic number 44 into a clearly named constant
(e.g., IOS_NAV_BAR_HEIGHT) and use it when computing contentInsetTop (where
isIOS26 is used) so the line const contentInsetTop = isIOS26 ? insets.top + 44 :
0 becomes insets.top + IOS_NAV_BAR_HEIGHT when applicable; also replace the same
literal in BookmarkLinkPreview (the other occurrence) or move the constant to a
shared module and import it to keep a single source of truth.
- Around line 29-30: The isIOS26 check uses a forced cast (Platform.Version as
string) which can mask runtime types; update the check to defensively handle
both string and number values for Platform.Version (e.g., use typeof
Platform.Version === "number" ? Platform.Version :
parseInt(String(Platform.Version), 10) or Number/Math.floor on the string) so
isIOS26 correctly evaluates on iOS regardless of whether Platform.Version is a
number or a string; replace the current isIOS26 assignment with this
type-guarded conversion referencing Platform.Version and isIOS26.

In `@apps/mobile/app/dashboard/settings/toolbar-settings.tsx`:
- Around line 25-34: The save callback currently closes over settings and
spreads it when calling setSettings, which can cause stale-overwrite; change
save to use the functional updater form of setSettings so you only update
toolbarActions and overflowActions based on the previous state (e.g., call
setSettings(prev => ({ ...prev, toolbarActions: nextVisible, overflowActions:
nextOverflow }))). Ensure setSettings supports a functional update; if not, note
the limitation or convert the setter to accept a functional updater.
- Around line 74-77: The TouchableOpacity acting as the drag handle (the
component with onLongPress={drag} and disabled={isActive}) needs accessibility
props so screen readers know it supports reordering; add accessible={true}, an
accessibilityRole (e.g., "button" or "adjustable"), an accessibilityLabel like
"Reorder item" and an accessibilityHint such as "Long press and drag to reorder
this item", and ensure the disabled state updates accessibilityState accordingly
so VoiceOver/TalkBack convey when dragging is unavailable.

In `@apps/mobile/components/bookmarks/BottomActions.tsx`:
- Around line 36-37: The isIOS26 platform check is duplicated; create a shared
constant (e.g., export const isIOS26 in a new apps/mobile/lib/platform.ts)
implementing Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >=
26, then replace the local const isIOS26 in BottomActions.tsx (and the duplicate
in dashboard/bookmarks/[slug]/index.tsx) with an import from that shared utility
and remove the duplicated declaration so both files use the single exported
isIOS26.
- Around line 203-227: The code saves images to a hardcoded temp filename
("temp_image.jpg") which breaks for non-JPEGs; change the logic in the
BookmarkTypes.ASSET branch (around fileUri, FileSystem.downloadAsync and
subsequent delete) to derive the file extension from the asset metadata or
response content-type before creating fileUri: use bookmark.content.fileName or
assetId metadata if available, otherwise perform a lightweight HEAD/fetch to
read the Content-Type header and map it to an extension (e.g., image/png ->
.png) with a safe fallback like .jpg; then construct fileUri using that
extension, call FileSystem.downloadAsync into that path, and keep the existing
shareAsync and deleteAsync flow.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 119efef and 497d21f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • apps/mobile/app/dashboard/(tabs)/(settings)/index.tsx
  • apps/mobile/app/dashboard/_layout.tsx
  • apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx
  • apps/mobile/app/dashboard/settings/toolbar-settings.tsx
  • apps/mobile/components/bookmarks/BookmarkHtmlHighlighterDom.tsx
  • apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx
  • apps/mobile/components/bookmarks/BookmarkLinkView.tsx
  • apps/mobile/components/bookmarks/BookmarkTextView.tsx
  • apps/mobile/components/bookmarks/BottomActions.tsx
  • apps/mobile/lib/settings.ts
  • apps/mobile/lib/useScrollDirection.ts
  • apps/mobile/package.json

esimkowitz and others added 2 commits February 26, 2026 22:47
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/mobile/components/bookmarks/BottomActions.tsx (1)

379-379: 🛠️ Refactor suggestion | 🟠 Major

Misleading naming: useGlassPill is not a hook.

The constant useGlassPill starts with use which suggests it's a React hook, but it's actually a module-level constant. This violates the Rules of Hooks naming convention.

-const useGlassPill = isIOS26 && isGlassEffectAPIAvailable();
+const shouldUseGlassPill = isIOS26 && isGlassEffectAPIAvailable();

Then update usages on lines 388 and 426.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/components/bookmarks/BottomActions.tsx` at line 379, The name
useGlassPill incorrectly looks like a React hook; rename the module-level
constant to a non-hook boolean name (for example isGlassPillEnabled or
showGlassPill) and update every usage of useGlassPill (the boolean
checks/conditionals and props that currently read useGlassPill) to the new name;
keep the value assignment the same (e.g., const isGlassPillEnabled = isIOS26 &&
isGlassEffectAPIAvailable()) and ensure all references (the two places that
currently read useGlassPill for rendering/logic) are updated accordingly.
🧹 Nitpick comments (1)
apps/mobile/components/bookmarks/BottomActions.tsx (1)

209-227: Potential race condition with static temp filename.

Using a hardcoded temp_image.jpg filename could cause issues if multiple share operations occur concurrently. Consider using a unique filename (e.g., with timestamp or bookmark ID).

♻️ Suggested fix with unique filename
-            const fileUri = `${FileSystem.documentDirectory}temp_image.jpg`;
+            const fileUri = `${FileSystem.documentDirectory}share_${bookmark.id}_${Date.now()}.jpg`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/components/bookmarks/BottomActions.tsx` around lines 209 - 227,
The code uses a hardcoded temp filename (`temp_image.jpg`) when building fileUri
in the BottomActions component (see variable fileUri and the
FileSystem.downloadAsync / Sharing.shareAsync / FileSystem.deleteAsync calls),
which can cause race conditions for concurrent share operations; change fileUri
to use a unique filename (e.g., include Date.now(), bookmark id, or a UUID) so
each downloadAsync target is distinct, then continue to share and delete that
unique path (ensure the same unique identifier is used for deleteAsync).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/mobile/components/bookmarks/BottomActions.tsx`:
- Around line 320-326: The favouriteBookmark mutation call is missing the
required bookmarkId argument which will cause the API call to fail; update the
onClick handler where favouriteBookmark is invoked (near Haptics.selectionAsync
and isFavourited) to pass the bookmarkId the same way archiveBookmark does
(include the bookmarkId property along with favourited: !isFavourited) so the
mutation receives both fields.

---

Duplicate comments:
In `@apps/mobile/components/bookmarks/BottomActions.tsx`:
- Line 379: The name useGlassPill incorrectly looks like a React hook; rename
the module-level constant to a non-hook boolean name (for example
isGlassPillEnabled or showGlassPill) and update every usage of useGlassPill (the
boolean checks/conditionals and props that currently read useGlassPill) to the
new name; keep the value assignment the same (e.g., const isGlassPillEnabled =
isIOS26 && isGlassEffectAPIAvailable()) and ensure all references (the two
places that currently read useGlassPill for rendering/logic) are updated
accordingly.

---

Nitpick comments:
In `@apps/mobile/components/bookmarks/BottomActions.tsx`:
- Around line 209-227: The code uses a hardcoded temp filename
(`temp_image.jpg`) when building fileUri in the BottomActions component (see
variable fileUri and the FileSystem.downloadAsync / Sharing.shareAsync /
FileSystem.deleteAsync calls), which can cause race conditions for concurrent
share operations; change fileUri to use a unique filename (e.g., include
Date.now(), bookmark id, or a UUID) so each downloadAsync target is distinct,
then continue to share and delete that unique path (ensure the same unique
identifier is used for deleteAsync).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 497d21f and 673187b.

📒 Files selected for processing (1)
  • apps/mobile/components/bookmarks/BottomActions.tsx

- Fix silent fall-through when sharing non-image/non-pdf assets
- Pass contentInsetTop to browser/archive WebViews for iOS 26
- Auto-inject missing action IDs into overflow on settings load
- Extract NAV_BAR_HEIGHT constant to replace magic number 44
- Remove unused ownerOnly/linkOnly from ToolbarActionMeta
- Extract triggerHaptic() helper to handle async catch
- Deduplicate image/PDF share logic with finally cleanup
- Add console.warn for unknown menu action events
- Simplify headerPlatformOptions to plain isIOS26 ternary
- Fix inaccurate comment about top/bottom padding
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx (1)

34-35: Duplicate NAV_BAR_HEIGHT constant.

This constant is also defined in apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx at line 33. Consider extracting it to a shared constants file to maintain a single source of truth.

♻️ Extract to shared constants

Create a shared constants file (e.g., apps/mobile/lib/constants.ts):

// Standard iOS navigation bar height (points)
export const NAV_BAR_HEIGHT = 44;

Then import it in both files:

-// Standard iOS navigation bar height (points)
-const NAV_BAR_HEIGHT = 44;
+import { NAV_BAR_HEIGHT } from "@/lib/constants";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx` around lines 34 -
35, Duplicate NAV_BAR_HEIGHT constant: remove the local constant from
BookmarkLinkPreview and the other file, create a single shared constants module
that exports NAV_BAR_HEIGHT, and update both BookmarkLinkPreview (component) and
the dashboard bookmarks page (index component) to import NAV_BAR_HEIGHT from
that module so both use the same source of truth.
apps/mobile/components/bookmarks/BottomActions.tsx (1)

175-201: Consider preserving original image file extension.

The temporary file for image sharing uses a hardcoded .jpg extension regardless of the actual asset type. While this works for most images, it could cause issues with PNGs or other formats when sharing to apps that respect MIME types based on extension.

🔧 Optional: Derive extension from asset metadata
           const fileUri =
             bookmark.content.assetType === "pdf"
               ? `${FileSystem.documentDirectory}${bookmark.content.fileName || "document.pdf"}`
-              : `${FileSystem.documentDirectory}temp_image.jpg`;
+              : `${FileSystem.documentDirectory}temp_image.${bookmark.content.fileName?.split('.').pop() || 'jpg'}`;

Alternatively, you could use a MIME type mapping if the asset metadata includes content type information.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/components/bookmarks/BottomActions.tsx` around lines 175 - 201,
The temp image filename currently hardcodes ".jpg" which can mis-label PNGs/etc;
update the logic that builds fileUri in the BottomActions component to derive
the extension and MIME from available metadata (use bookmark.content.fileName's
extension if present, or map bookmark.content.assetType/contentType to an
extension like .png/.jpg/.webp) and use that extension in fileUri, and pass the
correct mimeType/UTI into Sharing.shareAsync (references: fileUri,
bookmark.content.assetType, bookmark.content.fileName, FileSystem.downloadAsync,
Sharing.shareAsync, downloadResult.uri).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx`:
- Around line 34-35: Duplicate NAV_BAR_HEIGHT constant: remove the local
constant from BookmarkLinkPreview and the other file, create a single shared
constants module that exports NAV_BAR_HEIGHT, and update both
BookmarkLinkPreview (component) and the dashboard bookmarks page (index
component) to import NAV_BAR_HEIGHT from that module so both use the same source
of truth.

In `@apps/mobile/components/bookmarks/BottomActions.tsx`:
- Around line 175-201: The temp image filename currently hardcodes ".jpg" which
can mis-label PNGs/etc; update the logic that builds fileUri in the
BottomActions component to derive the extension and MIME from available metadata
(use bookmark.content.fileName's extension if present, or map
bookmark.content.assetType/contentType to an extension like .png/.jpg/.webp) and
use that extension in fileUri, and pass the correct mimeType/UTI into
Sharing.shareAsync (references: fileUri, bookmark.content.assetType,
bookmark.content.fileName, FileSystem.downloadAsync, Sharing.shareAsync,
downloadResult.uri).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89a25db and 967b83a.

📒 Files selected for processing (5)
  • apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx
  • apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx
  • apps/mobile/components/bookmarks/BookmarkLinkView.tsx
  • apps/mobile/components/bookmarks/BottomActions.tsx
  • apps/mobile/lib/settings.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/mobile/components/bookmarks/BookmarkTextView.tsx (1)

105-111: Avoid wiring scroll events when no listener is provided.

ScrollView currently emits high-frequency scroll events even when onScrollOffsetChange is absent. Make onScroll and scrollEventThrottle conditional to reduce unnecessary work.

♻️ Proposed refactor
     <ScrollView
       className="flex-1 rounded-lg border border-border bg-card p-2"
       contentInset={{ top: contentInsetTop }}
       contentOffset={{ x: 0, y: -contentInsetTop }}
-      scrollEventThrottle={16}
-      onScroll={(e) => onScrollOffsetChange?.(e.nativeEvent.contentOffset.y)}
+      scrollEventThrottle={onScrollOffsetChange ? 16 : undefined}
+      onScroll={
+        onScrollOffsetChange
+          ? (e) => onScrollOffsetChange(e.nativeEvent.contentOffset.y)
+          : undefined
+      }
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/components/bookmarks/BookmarkTextView.tsx` around lines 105 -
111, The ScrollView in BookmarkTextView.tsx is always wired to high-frequency
scroll events; change it so that onScroll and scrollEventThrottle are only set
when onScrollOffsetChange is provided: check the onScrollOffsetChange prop and,
if present, pass onScroll={(e) =>
onScrollOffsetChange?.(e.nativeEvent.contentOffset.y)} and
scrollEventThrottle={16}, otherwise omit both props (or set
scrollEventThrottle={0}/undefined) to avoid unnecessary work. Update the JSX
where ScrollView is rendered and reference the onScrollOffsetChange prop to gate
these props.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/mobile/components/bookmarks/BookmarkTextView.tsx`:
- Around line 105-111: The ScrollView in BookmarkTextView.tsx is always wired to
high-frequency scroll events; change it so that onScroll and scrollEventThrottle
are only set when onScrollOffsetChange is provided: check the
onScrollOffsetChange prop and, if present, pass onScroll={(e) =>
onScrollOffsetChange?.(e.nativeEvent.contentOffset.y)} and
scrollEventThrottle={16}, otherwise omit both props (or set
scrollEventThrottle={0}/undefined) to avoid unnecessary work. Update the JSX
where ScrollView is rendered and reference the onScrollOffsetChange prop to gate
these props.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 967b83a and a524c6e.

📒 Files selected for processing (1)
  • apps/mobile/components/bookmarks/BookmarkTextView.tsx

@esimkowitz esimkowitz force-pushed the mobile-reader-action-bar branch from 2ccece0 to f493cc6 Compare March 1, 2026 09:59
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.

FR: automatically hide top and bottom reader view bars FR: share button in reader view on mobile FR: customize bottom buttons in mobile reader view

1 participant