Skip to content

fix(mobile): use optimistic UI for toolbar favourite button#2811

Open
jreiml wants to merge 1 commit into
karakeep-app:mainfrom
jreiml:fix/optimistic-toolbar-favorite-button
Open

fix(mobile): use optimistic UI for toolbar favourite button#2811
jreiml wants to merge 1 commit into
karakeep-app:mainfrom
jreiml:fix/optimistic-toolbar-favorite-button

Conversation

@jreiml

@jreiml jreiml commented May 16, 2026

Copy link
Copy Markdown

Fixes #2810

Description

The favourite toolbar button in BottomActions.tsx rendered its icon from bookmark.favourited directly and set disabled: isFavouritePending. As a result, tapping the star showed no visible change until the mutation response came back, and rapid taps were silently dropped while the button was disabled.

Mirror the pattern already used by BookmarkCard's ActionBar:

  • Pull variables out of useUpdateBookmark and prefer variables.favourited over bookmark.favourited when present, so the icon reflects the in-flight value immediately.
  • Drop disabled: isFavouritePending so subsequent taps register and toggle.

The mutation's onError toast still surfaces failures, so the optimistic flip remains safe.

How Has This Been Tested?

  • Tested on Android (Pixel 6a) — star now flips on tap; rapid double-tap toggles cleanly

Checklist:

  • I have carefully read CONTRIBUTING.md
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation if applicable
  • I have no unrelated changes in the PR.
  • I have confirmed that any new dependencies are strictly necessary.
  • I have written tests for new code (if applicable)

Please describe to which degree, if any, an LLM was used in creating this pull request.

Claude (Opus 4.7 via Claude Code) identified the discrepancy with BookmarkCard's ActionBar and authored the change. The bug was discovered and verified on a physical Android device by the contributor.

The favourite button was disabled while the mutation was in flight and
the icon only updated once the response came back, so rapid taps were
silently dropped and the star did not flip until the network round-trip
completed. Mirror the optimistic pattern already used by BookmarkCard's
ActionBar so the icon flips immediately and additional taps register.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The favorite toolbar action in BottomActions is updated to use mutation variables for optimistic UI state. The mutation hook now captures variables instead of pending status, and the action renders the star icon based on the mutation's favorited state with a fallback to the bookmark's current state. The action is no longer disabled during mutation operations.

Changes

Favorite Action Optimistic State

Layer / File(s) Summary
Favorite Action State and Disabled Behavior
apps/mobile/components/bookmarks/BottomActions.tsx
The favorite mutation hook destructuring changes from capturing isPending to capturing variables (e.g., favouriteVariables). The favorite action's icon state and toggle value are derived from favouriteVariables.favourited when available, with fallback to bookmark.favourited. The action's disabled behavior is removed entirely, keeping it always enabled rather than reflecting the mutation's pending state.

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and concisely summarizes the main change: implementing optimistic UI for the toolbar favourite button, which directly matches the core objective of the pull request.
Description check ✅ Passed The description comprehensively addresses all template sections including problem statement, solution approach, testing confirmation, and a completed checklist with LLM usage disclosure.
Linked Issues check ✅ Passed The pull request directly addresses issue #2810 by implementing optimistic UI for the favourite button using mutation variables and removing the disabled state, enabling rapid taps and immediate visual feedback.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the favourite button behavior in BottomActions.tsx as specified in issue #2810; no unrelated modifications are present.

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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

greptile-apps Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the toolbar favourite button in the mobile app to use optimistic UI, so the star icon flips immediately on tap rather than waiting for the server response. The change mirrors the existing pattern already in use by BookmarkCard's ActionBar.

  • Replaces isPending-based disabling with variables.favourited from useUpdateBookmark, so the icon reflects the in-flight value and rapid taps are no longer silently dropped.
  • The onClick toggle also reads from favouriteVariables (current optimistic state) rather than bookmark.favourited (server state), which is a subtle improvement over the BookmarkCard pattern — rapid double-tapping correctly sends opposite values instead of repeating the same one.

Confidence Score: 5/5

Safe to merge — a narrow, well-understood change to a single action button with on-device verification reported.

The change is small and isolated to the favourite action in BottomActions. The optimistic pattern it introduces is already established in BookmarkCard, the error path still surfaces a toast, and the toggle logic correctly reads from the in-flight variables state rather than stale server state. No correctness issues were found.

No files require special attention.

Important Files Changed

Filename Overview
apps/mobile/components/bookmarks/BottomActions.tsx Switches favourite button to optimistic UI by reading variables.favourited in-flight; drops isFavouritePending disable guard. Mirrors the pattern in BookmarkCard's ActionBar.

Reviews (1): Last reviewed commit: "fix(mobile): use optimistic UI for toolb..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@apps/mobile/components/bookmarks/BottomActions.tsx`:
- Line 133: The optimistic-favourite UI is being driven directly from
favouriteVariables which persists after the mutation completes; change the UI
logic to only use the optimistic value when the mutation is still in-flight and
targeting the same bookmark by gating on isPending (or the hook's pending flag)
AND favouriteVariables?.id === bookmark.id, otherwise fall back to
bookmark.favourited; ensure you call reset() after handling completion/errors so
stale favouriteVariables are cleared. Apply the same pattern to the other
mutation hooks in this file that destructure { mutate: ..., variables: ... }
(the other occurrences flagged in the review) so each optimistic state is only
used while pending and for the matching bookmark id.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e386af0a-5476-4edb-9cd4-3974fb90c05f

📥 Commits

Reviewing files that changed from the base of the PR and between 4b22229 and 07a28ac.

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

});

const { mutate: favouriteBookmark, isPending: isFavouritePending } =
const { mutate: favouriteBookmark, variables: favouriteVariables } =

@coderabbitai coderabbitai Bot May 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In TanStack Query v5, does useMutation().variables remain set after success/error until reset(), or is it only reliable while isPending is true?

💡 Result:

In TanStack Query v5, the variables property returned by useMutation persists after the mutation completes (whether it succeeds or fails) until the mutation state is explicitly cleared [1][2][3]. It is not limited to being reliable only while isPending is true. The variables property reflects the arguments passed to the most recent invocation of the mutate function [1][2]. This state remains available in the useMutation result object until you call the reset() function, which clears the mutation state (setting data, error, and variables back to their initial states) [1][2][4]. Because variables persists, it is commonly used in callbacks like onSuccess or onError to access the data that triggered the mutation, even after the mutation has finished [5][6][3]. If you need to clear this state to prevent stale data from being accessed in your UI or logic, you should call reset() [4].

Citations:


🏁 Script executed:

# Locate the file
fd "BottomActions.tsx" --type f

Repository: karakeep-app/karakeep

Length of output: 116


🏁 Script executed:

# Read the file to verify line numbers and context
wc -l apps/mobile/components/bookmarks/BottomActions.tsx

Repository: karakeep-app/karakeep

Length of output: 120


🏁 Script executed:

# Read lines around 133 to see the mutation setup
sed -n '130,145p' apps/mobile/components/bookmarks/BottomActions.tsx

Repository: karakeep-app/karakeep

Length of output: 446


🏁 Script executed:

# Read lines around 221-223 to see the first usage
sed -n '218,226p' apps/mobile/components/bookmarks/BottomActions.tsx

Repository: karakeep-app/karakeep

Length of output: 317


🏁 Script executed:

# Read lines around 231-233 to see the second usage
sed -n '228,240p' apps/mobile/components/bookmarks/BottomActions.tsx

Repository: karakeep-app/karakeep

Length of output: 394


🏁 Script executed:

# Search for useUpdateBookmark hook definition
rg "useUpdateBookmark" --type ts --type tsx -B 2 -A 10 | head -50

Repository: karakeep-app/karakeep

Length of output: 93


🏁 Script executed:

# Search for reset() calls in the file to see if mutation state is cleared
rg "reset\(\)" apps/mobile/components/bookmarks/BottomActions.tsx -B 2 -A 2

Repository: karakeep-app/karakeep

Length of output: 47


🏁 Script executed:

# Locate the hook definition file
fd "useUpdateBookmark" --type f

Repository: karakeep-app/karakeep

Length of output: 47


🏁 Script executed:

# Search for useUpdateBookmark definition across the codebase
rg "useUpdateBookmark" -A 20 | head -80

Repository: karakeep-app/karakeep

Length of output: 5834


🏁 Script executed:

# Look in mobile app hooks directory
fd "hook" -t d apps/mobile

Repository: karakeep-app/karakeep

Length of output: 47


🏁 Script executed:

# Search for files containing useUpdateBookmark
rg "export.*useUpdateBookmark\|function useUpdateBookmark\|const useUpdateBookmark" -A 15

Repository: karakeep-app/karakeep

Length of output: 47


🏁 Script executed:

# Read the full useUpdateBookmark implementation
sed -n '/^export function useUpdateBookmark/,/^}/p' packages/shared-react/hooks/bookmarks.ts

Repository: karakeep-app/karakeep

Length of output: 2091


Gate optimistic favourite state to isPending and matching bookmark ID to avoid stale UI state.

favouriteVariables persists after the mutation settles (success or error) until reset() is explicitly called. Using it unconditionally pins the star icon to the last submitted value even after the mutation completes, causing the UI to desync from bookmark.favourited when mutations fail, the server normalizes differently, or the bookmark prop changes.

Suggested patch
-  const { mutate: favouriteBookmark, variables: favouriteVariables } =
+  const {
+    mutate: favouriteBookmark,
+    isPending: isFavouritePending,
+    variables: favouriteVariables,
+  } =
     useUpdateBookmark({
       onError: () => {
         toast({
           message: "Something went wrong",
           variant: "destructive",
           showProgress: false,
         });
       },
     });
+
+  const optimisticFavourited =
+    isFavouritePending && favouriteVariables?.bookmarkId === bookmark.id
+      ? favouriteVariables.favourited
+      : bookmark.favourited;
@@
-      icon: (
-        favouriteVariables ? favouriteVariables.favourited : bookmark.favourited
-      )
+      icon: optimisticFavourited
         ? makeIcon(Star, "`#ebb434`", "`#ebb434`")
         : makeIcon(Star),
@@
         favouriteBookmark({
           bookmarkId: bookmark.id,
-          favourited: !(favouriteVariables
-            ? favouriteVariables.favourited
-            : bookmark.favourited),
+          favourited: !optimisticFavourited,
         });
       },

Also applies to: 221-223, 231-233

🤖 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 `@apps/mobile/components/bookmarks/BottomActions.tsx` at line 133, The
optimistic-favourite UI is being driven directly from favouriteVariables which
persists after the mutation completes; change the UI logic to only use the
optimistic value when the mutation is still in-flight and targeting the same
bookmark by gating on isPending (or the hook's pending flag) AND
favouriteVariables?.id === bookmark.id, otherwise fall back to
bookmark.favourited; ensure you call reset() after handling completion/errors so
stale favouriteVariables are cleared. Apply the same pattern to the other
mutation hooks in this file that destructure { mutate: ..., variables: ... }
(the other occurrences flagged in the review) so each optimistic state is only
used while pending and for the matching bookmark id.

@jreiml jreiml May 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same issue exists in the main bookmarks view, should that be changed too?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find BookmarkCard related files
fd "BookmarkCard" --type f

# Also look for ActionBar in mobile components
fd "ActionBar" --type f apps/mobile

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Search for useUpdateBookmark and variables usage in BookmarkCard.tsx
rg "useUpdateBookmark|variables|favourited|isPending|isFavourit" apps/mobile/components/bookmarks/BookmarkCard.tsx -n -A 3 -B 1

Length of output: 1067


🏁 Script executed:

#!/bin/bash
# Get full context around lines 165-185 in BookmarkCard.tsx
sed -n '165,190p' apps/mobile/components/bookmarks/BookmarkCard.tsx

Length of output: 710


@jreiml Yes — apps/mobile/components/bookmarks/BookmarkCard.tsx has the exact same pattern. At Line 72 it captures variables but not isPending:

const { mutate: favouriteBookmark, variables } = useUpdateBookmark({

And at Line 176 it uses variables without any isPending or bookmarkId guard:

{(variables ? variables.favourited : bookmark.favourited) ? (
  <Star fill="`#ebb434`" color="`#ebb434`" />
) : (
  <Star color="gray" />
)}

There's actually a second, pre-existing bug there too: the favourite toggle (Line 172) reads !bookmark.favourited instead of the optimistic value, so rapid double-taps would re-submit the same value rather than toggling:

favouriteBookmark({
  bookmarkId: bookmark.id,
  favourited: !bookmark.favourited,  // ← should use optimistic value
});

This PR's BottomActions.tsx already fixed that second issue correctly by using !(favouriteVariables ? favouriteVariables.favourited : bookmark.favourited).

It would be worth fixing BookmarkCard.tsx with the same isPending + bookmarkId guard suggested in this review. Would you like me to open a follow-up issue to track this?


🧠 Learnings used
Learnt from: esimkowitz
Repo: karakeep-app/karakeep PR: 2365
File: apps/mobile/components/reader/ReaderPreview.tsx:35-47
Timestamp: 2026-01-10T01:57:00.142Z
Learning: In React Native WebView usage, if the WebView renders only inline, app-controlled HTML with constants and no external navigation or user-supplied content, many WebView security hardening measures (restrict originWhitelist, navigation gating, escaping in injectJavaScript) are less critical for this code path. Maintain awareness, but apply this judgment specifically to files that render controlled content; for any WebView delegate handling external data, reassess and re-enable appropriate security controls.

Learnt from: esimkowitz
Repo: karakeep-app/karakeep PR: 2685
File: apps/mobile/components/ui/AndroidSearchBar.tsx:8-16
Timestamp: 2026-04-12T06:09:29.362Z
Learning: In this codebase’s Expo/React Native app under `apps/mobile`, it is an accepted convention to use `React.ReactNode` without an explicit `import React` in `.tsx` files. Because the Expo TypeScript/JSX setup uses the `react-jsx` transform, React types are available and this omission should not be flagged as an issue. Only flag missing React imports for patterns that are not covered by this `react-jsx`/type availability behavior.

@MohamedBassem

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 07a28ac58a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

});
},
disabled: isFavouritePending,
disabled: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Serialize favourite toggles to preserve last user intent

Allowing the favourite action to stay enabled during an in-flight mutation causes consecutive taps to dispatch multiple updateBookmark requests in parallel, and those requests can complete out of order; when that happens, the server may persist an older toggle instead of the final tap. This is a functional regression from the prior guarded behavior and can leave the bookmark in the opposite state from what the user last selected under normal high-latency/mobile conditions.

Useful? React with 👍 / 👎.

Comment on lines +222 to +223
favouriteVariables ? favouriteVariables.favourited : bookmark.favourited
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset optimistic favourite UI when mutation fails

The icon now always prefers favouriteVariables.favourited over bookmark.favourited, but onError only shows a toast and never clears mutation state, so a failed request can leave the star visually toggled even though the backend update did not apply. In that failure path, subsequent taps are computed from stale optimistic data, which can produce no-op requests and confusing UI until a full refetch/remount.

Useful? React with 👍 / 👎.

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.

[Mobile] Reader toolbar favourite button doesn't update until network round-trip; rapid taps dropped

2 participants