From a39637f187ffc62aa9c412419a3050a65c3ddd93 Mon Sep 17 00:00:00 2001 From: Simon Eklundh Date: Sat, 4 Jul 2026 12:12:32 +0200 Subject: [PATCH] blocks repeated watchlist actions so one happens at a time, and gates watchlist behind user id + item --- components/AddToKefinWatchlist.tsx | 3 ++- components/RoundButton.tsx | 23 +++++++++++++++++------ components/tv/TVWatchlistButton.tsx | 4 ++-- hooks/useWatchlist.ts | 23 +++++++++++++---------- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/components/AddToKefinWatchlist.tsx b/components/AddToKefinWatchlist.tsx index 351f6fcd..952545bd 100644 --- a/components/AddToKefinWatchlist.tsx +++ b/components/AddToKefinWatchlist.tsx @@ -13,7 +13,7 @@ interface Props extends ViewProps { * Render only when settings.useKefinTweaks is enabled. */ export const AddToKefinWatchlist: FC = ({ item, ...props }) => { - const { isWatchlisted, toggleWatchlist } = useWatchlist(item); + const { isWatchlisted, toggleWatchlist, isPending } = useWatchlist(item); return ( @@ -22,6 +22,7 @@ export const AddToKefinWatchlist: FC = ({ item, ...props }) => { icon={isWatchlisted ? "bookmark" : "bookmark-outline"} color={isWatchlisted ? "purple" : "white"} onPress={toggleWatchlist} + disabled={isPending} /> ); diff --git a/components/RoundButton.tsx b/components/RoundButton.tsx index 5493d94f..40860bfb 100644 --- a/components/RoundButton.tsx +++ b/components/RoundButton.tsx @@ -13,6 +13,7 @@ interface Props extends ViewProps { fillColor?: "primary"; color?: "white" | "purple"; hapticFeedback?: boolean; + disabled?: boolean; } export const RoundButton: React.FC> = ({ @@ -24,6 +25,7 @@ export const RoundButton: React.FC> = ({ fillColor, color = "white", hapticFeedback = true, + disabled = false, ...viewProps }) => { const buttonSize = size === "large" ? "h-10 w-10" : "h-9 w-9"; @@ -31,6 +33,7 @@ export const RoundButton: React.FC> = ({ const lightHapticFeedback = useHaptic("light"); const handlePress = () => { + if (disabled) return; if (hapticFeedback) { lightHapticFeedback(); } @@ -41,7 +44,8 @@ export const RoundButton: React.FC> = ({ return ( {icon ? ( @@ -60,7 +64,8 @@ export const RoundButton: React.FC> = ({ return ( {icon ? ( @@ -78,7 +83,8 @@ export const RoundButton: React.FC> = ({ return ( {icon ? ( @@ -96,9 +102,10 @@ export const RoundButton: React.FC> = ({ return ( {icon ? ( @@ -113,10 +120,14 @@ export const RoundButton: React.FC> = ({ ); return ( - + {icon ? ( diff --git a/components/tv/TVWatchlistButton.tsx b/components/tv/TVWatchlistButton.tsx index 0244a6cf..f43a0dfe 100644 --- a/components/tv/TVWatchlistButton.tsx +++ b/components/tv/TVWatchlistButton.tsx @@ -17,14 +17,14 @@ export const TVWatchlistButton: React.FC = ({ item, disabled, }) => { - const { isWatchlisted, toggleWatchlist } = useWatchlist(item); + const { isWatchlisted, toggleWatchlist, isPending } = useWatchlist(item); return ( { const [user] = useAtom(userAtom); const [watchlist, setWatchlist] = useAtom(watchlistAtom); - const itemId = item.Id ?? ""; + const watchlistKey = user?.Id && item.Id ? `${user.Id}:${item.Id}` : ""; // Get current watchlist status from shared state, falling back to item data - const isWatchlisted = itemId - ? (watchlist[itemId] ?? item.UserData?.Likes) + const isWatchlisted = watchlistKey + ? (watchlist[watchlistKey] ?? item.UserData?.Likes) : item.UserData?.Likes; // Update shared state when item data changes useEffect(() => { - if (itemId && item.UserData?.Likes !== undefined) { + if (watchlistKey && item.UserData?.Likes !== undefined) { setWatchlist((prev) => ({ ...prev, - [itemId]: item.UserData!.Likes!, + [watchlistKey]: item.UserData!.Likes!, })); } - }, [itemId, item.UserData?.Likes, setWatchlist]); - + }, [watchlistKey, item.UserData?.Likes, setWatchlist]); // Helper to update watchlist status in shared state const setIsWatchlisted = useCallback( (value: boolean | null | undefined) => { - if (itemId && typeof value === "boolean") { - setWatchlist((prev) => ({ ...prev, [itemId]: value })); + if (watchlistKey && typeof value === "boolean") { + +setWatchlist((prev) => ({ ...prev, [watchlistKey]: value })); } }, - [itemId, setWatchlist], + [watchlistKey, setWatchlist], ); // Use refs to avoid stale closure issues in mutationFn @@ -142,12 +141,16 @@ export const useWatchlist = (item: BaseItemDto) => { }); const toggleWatchlist = useCallback(() => { + // Ignore taps while a flip is in flight so overlapping requests can't + // race and leave Jellyfin's Likes value out of sync with the UI. + if (watchlistMutation.isPending) return; watchlistMutation.mutate(!isWatchlisted); }, [watchlistMutation, isWatchlisted]); return { isWatchlisted, toggleWatchlist, + isPending: watchlistMutation.isPending, watchlistMutation, }; };