diff --git a/apps/rebreak-native/components/PostCard.tsx b/apps/rebreak-native/components/PostCard.tsx index 8b6e6ce..9132139 100644 --- a/apps/rebreak-native/components/PostCard.tsx +++ b/apps/rebreak-native/components/PostCard.tsx @@ -26,8 +26,20 @@ function PostCardImpl({ post, onCommentPress }: Props) { const revertOptimisticLike = useCommunityStore((s) => s.revertOptimisticLike); const clearOptimisticLike = useCommunityStore((s) => s.clearOptimisticLike); - const [localLike, setLocalLike] = useState<'like' | null>(post.userLike === 'like' ? 'like' : null); - const [localCount, setLocalCount] = useState(post.likesCount); + // Display state is DERIVED from props + the store-level optimistic delta. + // No useState mirror — that's exactly what created the foreign-likes-don't- + // update bug (seed-once from props) and the post-action flicker (useEffect + // re-sync racing with the React-Query cache patch). Realtime patches the + // cache → prop changes → we re-render automatically. Optimistic UI lives + // for the ms between tap and API-response in the community-store's + // `optimisticLikes` map (delta + userLike). + const optimistic = useCommunityStore((s) => s.optimisticLikes[post.id]); + const displayedLike: 'like' | null = optimistic + ? optimistic.userLike + : post.userLike === 'like' + ? 'like' + : null; + const displayedCount = (post.likesCount ?? 0) + (optimistic?.delta ?? 0); const [isLiking, setIsLiking] = useState(false); // Heart-Pop Animation — Insta-Style: quick scale-up + spring-bounce back @@ -136,9 +148,7 @@ function PostCardImpl({ post, onCommentPress }: Props) { const handleLike = useCallback(async () => { if (isLiking) return; triggerHeartPop(); - const { newLike, newCount } = applyOptimisticLike(post.id, localLike, localCount); - setLocalLike(newLike); - setLocalCount(newCount); + applyOptimisticLike(post.id, displayedLike, displayedCount); setIsLiking(true); try { const res = await apiFetch<{ @@ -149,19 +159,29 @@ function PostCardImpl({ post, onCommentPress }: Props) { method: 'POST', body: { postId: post.id, type: 'like' }, }); - setLocalCount(res.likesCount); - setLocalLike(res.userLike === 'like' ? 'like' : null); + // Patch the React-Query cache synchronously with server truth so the + // prop reflects it BEFORE we clear the optimistic delta. Without this, + // there'd be a millisecond window between clearOptimistic (delta → 0) + // and the Realtime broadcast patching the cache — during which the + // displayed count would briefly snap back to the old prop value. + queryClient.setQueriesData( + { queryKey: ['community-posts'] }, + (data) => + Array.isArray(data) + ? data.map((p) => + p.id === post.id + ? { ...p, likesCount: res.likesCount, dislikesCount: res.dislikesCount, userLike: res.userLike } + : p, + ) + : data, + ); clearOptimisticLike(post.id); - // KEIN queryClient.invalidateQueries — würde die komplette Liste neu laden, - // PostCard remounted, Heart-Pop-Animation abgebrochen. Local-State reicht. } catch { revertOptimisticLike(post.id); - setLocalLike(post.userLike === 'like' ? 'like' : null); - setLocalCount(post.likesCount); } finally { setIsLiking(false); } - }, [isLiking, localLike, localCount, post.id, post.userLike, post.likesCount, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]); + }, [isLiking, displayedLike, displayedCount, post.id, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]); return ( @@ -278,13 +298,13 @@ function PostCardImpl({ post, onCommentPress }: Props) { > - {localCount > 0 && ( - {localCount} + {displayedCount > 0 && ( + {displayedCount} )}