Skip to content

Fix favorite/watched toggle appearing to no-op on repeat taps - #84

Merged
imbenjamin merged 1 commit into
developfrom
fix/favorite-watched-toggle-staleness
Aug 16, 2026
Merged

Fix favorite/watched toggle appearing to no-op on repeat taps#84
imbenjamin merged 1 commit into
developfrom
fix/favorite-watched-toggle-staleness

Conversation

@imbenjamin

Copy link
Copy Markdown
Owner

Summary

On asset detail pages, tapping Favorite/Watched once worked, but a second tap looked like it did nothing — no icon change, and no way to tell if the server got the update.

Root cause was two compounding bugs, found by reproducing live in the Simulator against a real Jellyfin server and checking server-side truth via curl at each step (not just what the UI showed):

  1. Stale closure in the toolbar button. HeroActionButtons is hosted as a ToolbarItem. Its displayed icon reliably tracks the view model reactively, but its Button/Menu row action closures could keep firing with the MediaItem snapshot captured whenever that toolbar content was first built — not the latest render. Confirmed directly: the icon visibly flipped after tap 1, but tap 2's currentlyFavorite still read the value from before tap 1, so it silently repeated the same write instead of reversing it.

    Fixed by having the toggle look up current status through AssetDetailViewModel (currentFavoriteWatchedStatus(forItemID:)) fresh at the moment it fires, rather than trusting the closure's captured value.

  2. Slow server-side commit outlasting the confirmation poll. Jellyfin's favorite/watched write endpoints return success immediately but commit the actual userData change asynchronously. Confirmed live that this can take several minutes on a real server — an order of magnitude past the app's ~13s confirmation-poll budget. When that happened, the poll kept patching item with the still-stale fetched value on every attempt, so once it gave up, the button looked exactly like the tap had done nothing.

    Fixed with an optimistic update (MediaItem.withOptimisticFavoriteWatched, applied the moment the write itself succeeds) plus tightening the poll to only ever adopt a confirmed fetch — it can no longer regress the optimistic value back to stale data mid-poll.

Also closed a related gap: the Show-content page's Menu rows (Show/Season/Episode) didn't have the plain button's .disabled(isPending) guard, so re-opening the menu mid-toggle could fire a second, concurrent write.

Testing

  • Full suite: 442 tests, 0 failures.
  • Two new AssetDetailViewModelTests pin the optimistic-update-survives-an-unconfirmed-poll behavior and the new currentFavoriteWatchedStatus(forItemID:) lookup.
  • Two new MediaItemTests cover withOptimisticFavoriteWatched.
  • Manually reproduced the original bug and confirmed the fix in the iOS Simulator against a real Jellyfin server (repeated favorite/watched taps on both a Movie page and a Show page's extended menu, cross-checking server state via curl after each tap).

🤖 Generated with Claude Code

Two compounding bugs on asset detail pages' Favorite/Watched buttons:

1. HeroActionButtons' toolbar-hosted Button/Menu-row action closures
   could keep firing with the MediaItem snapshot captured whenever that
   toolbar content was first built, not the latest render — confirmed
   live: the icon itself updated correctly after a tap, but the *next*
   tap's currentlyFavorite still read the pre-first-tap value, silently
   repeating the same write instead of reversing it. Fixed by having the
   toggle read current status through AssetDetailViewModel directly
   (currentFavoriteWatchedStatus(forItemID:)) at the moment it fires,
   rather than trusting the closure's own captured value.

2. Jellyfin's favorite/watched write endpoints return success
   immediately but commit the userData change asynchronously — confirmed
   live that this can take several *minutes* on a real server, an order
   of magnitude past the ~13s confirmation-poll budget. When that
   happened, the poll kept patching `item` with the still-stale fetched
   value on every attempt and gave up showing it — indistinguishable
   from the tap having done nothing. Fixed with an optimistic update
   (MediaItem.withOptimisticFavoriteWatched, applied the moment the
   write itself succeeds) plus tightening the poll to only ever adopt a
   confirmed fetch, so it can no longer regress the optimistic value
   back to stale data mid-poll.

Also added the Menu-row (Show-content page) equivalent of the plain
button's .disabled(isPending) guard, closing a related gap where
re-opening the menu mid-toggle could fire a second concurrent write.

Root-caused in the Simulator against a real Jellyfin server, confirming
server-side truth via curl at each step rather than trusting the UI.
@imbenjamin
imbenjamin merged commit 270f4a5 into develop Aug 16, 2026
2 checks passed
@imbenjamin
imbenjamin deleted the fix/favorite-watched-toggle-staleness branch August 16, 2026 19:17
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