Revert "fix(native/community): derive heart state from props + store-optimistic delta"

This reverts commit d28d1f145d9bdaa45fb788aaef69c645719f56bb.
This commit is contained in:
chahinebrini 2026-05-16 00:48:14 +02:00
parent 6f760f3aea
commit 7c6b463acb

View File

@ -26,20 +26,8 @@ function PostCardImpl({ post, onCommentPress }: Props) {
const revertOptimisticLike = useCommunityStore((s) => s.revertOptimisticLike); const revertOptimisticLike = useCommunityStore((s) => s.revertOptimisticLike);
const clearOptimisticLike = useCommunityStore((s) => s.clearOptimisticLike); const clearOptimisticLike = useCommunityStore((s) => s.clearOptimisticLike);
// Display state is DERIVED from props + the store-level optimistic delta. const [localLike, setLocalLike] = useState<'like' | null>(post.userLike === 'like' ? 'like' : null);
// No useState mirror — that's exactly what created the foreign-likes-don't- const [localCount, setLocalCount] = useState(post.likesCount);
// 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); const [isLiking, setIsLiking] = useState(false);
// Heart-Pop Animation — Insta-Style: quick scale-up + spring-bounce back // Heart-Pop Animation — Insta-Style: quick scale-up + spring-bounce back
@ -148,7 +136,9 @@ function PostCardImpl({ post, onCommentPress }: Props) {
const handleLike = useCallback(async () => { const handleLike = useCallback(async () => {
if (isLiking) return; if (isLiking) return;
triggerHeartPop(); triggerHeartPop();
applyOptimisticLike(post.id, displayedLike, displayedCount); const { newLike, newCount } = applyOptimisticLike(post.id, localLike, localCount);
setLocalLike(newLike);
setLocalCount(newCount);
setIsLiking(true); setIsLiking(true);
try { try {
const res = await apiFetch<{ const res = await apiFetch<{
@ -159,29 +149,19 @@ function PostCardImpl({ post, onCommentPress }: Props) {
method: 'POST', method: 'POST',
body: { postId: post.id, type: 'like' }, body: { postId: post.id, type: 'like' },
}); });
// Patch the React-Query cache synchronously with server truth so the setLocalCount(res.likesCount);
// prop reflects it BEFORE we clear the optimistic delta. Without this, setLocalLike(res.userLike === 'like' ? 'like' : null);
// 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<CommunityPost[]>(
{ 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); clearOptimisticLike(post.id);
// KEIN queryClient.invalidateQueries — würde die komplette Liste neu laden,
// PostCard remounted, Heart-Pop-Animation abgebrochen. Local-State reicht.
} catch { } catch {
revertOptimisticLike(post.id); revertOptimisticLike(post.id);
setLocalLike(post.userLike === 'like' ? 'like' : null);
setLocalCount(post.likesCount);
} finally { } finally {
setIsLiking(false); setIsLiking(false);
} }
}, [isLiking, displayedLike, displayedCount, post.id, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]); }, [isLiking, localLike, localCount, post.id, post.userLike, post.likesCount, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]);
return ( return (
<View style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 16, padding: 12, marginBottom: 12 }}> <View style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 16, padding: 12, marginBottom: 12 }}>
@ -298,13 +278,13 @@ function PostCardImpl({ post, onCommentPress }: Props) {
> >
<Animated.View style={{ transform: [{ scale: heartScale }] }}> <Animated.View style={{ transform: [{ scale: heartScale }] }}>
<Ionicons <Ionicons
name={displayedLike === 'like' ? 'heart' : 'heart-outline'} name={localLike === 'like' ? 'heart' : 'heart-outline'}
size={20} size={20}
color={displayedLike === 'like' ? '#dc2626' : '#737373'} color={localLike === 'like' ? '#dc2626' : '#737373'}
/> />
</Animated.View> </Animated.View>
{displayedCount > 0 && ( {localCount > 0 && (
<Text className="text-xs text-neutral-600" style={{ fontFamily: 'Nunito_600SemiBold' }}>{displayedCount}</Text> <Text className="text-xs text-neutral-600" style={{ fontFamily: 'Nunito_600SemiBold' }}>{localCount}</Text>
)} )}
</Pressable> </Pressable>