- Extract ToggleCommentReactionVars and ToggleIssueReactionVars shared types so mutation definitions and useMutationState consumers stay in sync without as-casts on inline types - Replace new Date().toISOString() with empty string in optimistic reaction objects to avoid unstable references in useMemo Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
138 lines
4 KiB
TypeScript
138 lines
4 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useMemo } from "react";
|
|
import { useQuery, useQueryClient, useMutationState } from "@tanstack/react-query";
|
|
import type { IssueReaction } from "@/shared/types";
|
|
import type {
|
|
IssueReactionAddedPayload,
|
|
IssueReactionRemovedPayload,
|
|
} from "@/shared/types";
|
|
import { issueReactionsOptions, issueKeys } from "@core/issues/queries";
|
|
import { useToggleIssueReaction, type ToggleIssueReactionVars } from "@core/issues/mutations";
|
|
import { useWSEvent, useWSReconnect } from "@/features/realtime";
|
|
|
|
export function useIssueReactions(issueId: string, userId?: string) {
|
|
const qc = useQueryClient();
|
|
const { data: serverReactions = [], isLoading: loading } = useQuery(
|
|
issueReactionsOptions(issueId),
|
|
);
|
|
|
|
const toggleMutation = useToggleIssueReaction(issueId);
|
|
|
|
// Reconnect recovery
|
|
useWSReconnect(
|
|
useCallback(() => {
|
|
qc.invalidateQueries({ queryKey: issueKeys.reactions(issueId) });
|
|
}, [qc, issueId]),
|
|
);
|
|
|
|
// --- WS event handlers (update server cache for other users' actions) ---
|
|
|
|
useWSEvent(
|
|
"issue_reaction:added",
|
|
useCallback(
|
|
(payload: unknown) => {
|
|
const { reaction, issue_id } = payload as IssueReactionAddedPayload;
|
|
if (issue_id !== issueId) return;
|
|
qc.setQueryData<IssueReaction[]>(
|
|
issueKeys.reactions(issueId),
|
|
(old) => {
|
|
if (!old) return old;
|
|
if (old.some((r) => r.id === reaction.id)) return old;
|
|
return [...old, reaction];
|
|
},
|
|
);
|
|
},
|
|
[qc, issueId],
|
|
),
|
|
);
|
|
|
|
useWSEvent(
|
|
"issue_reaction:removed",
|
|
useCallback(
|
|
(payload: unknown) => {
|
|
const p = payload as IssueReactionRemovedPayload;
|
|
if (p.issue_id !== issueId) return;
|
|
qc.setQueryData<IssueReaction[]>(
|
|
issueKeys.reactions(issueId),
|
|
(old) =>
|
|
old?.filter(
|
|
(r) =>
|
|
!(
|
|
r.emoji === p.emoji &&
|
|
r.actor_type === p.actor_type &&
|
|
r.actor_id === p.actor_id
|
|
),
|
|
),
|
|
);
|
|
},
|
|
[qc, issueId],
|
|
),
|
|
);
|
|
|
|
// --- Optimistic UI derivation ---
|
|
// Instead of writing temp data into the cache (which races with WS events),
|
|
// derive optimistic state at render time from pending mutation variables.
|
|
|
|
const pendingVars = useMutationState({
|
|
filters: {
|
|
mutationKey: ["toggleIssueReaction", issueId],
|
|
status: "pending",
|
|
},
|
|
select: (m) =>
|
|
m.state.variables as ToggleIssueReactionVars | undefined,
|
|
});
|
|
|
|
const reactions = useMemo(() => {
|
|
if (pendingVars.length === 0) return serverReactions;
|
|
|
|
let result = [...serverReactions];
|
|
for (const vars of pendingVars) {
|
|
if (!vars) continue;
|
|
if (vars.existing) {
|
|
// Pending removal
|
|
result = result.filter((r) => r.id !== vars.existing!.id);
|
|
} else {
|
|
// Pending add — skip if server already has it (WS arrived first)
|
|
const alreadyExists = result.some(
|
|
(r) =>
|
|
r.emoji === vars.emoji &&
|
|
r.actor_type === "member" &&
|
|
r.actor_id === userId,
|
|
);
|
|
if (!alreadyExists) {
|
|
result = [
|
|
...result,
|
|
{
|
|
id: `optimistic-${vars.emoji}`,
|
|
issue_id: issueId,
|
|
actor_type: "member",
|
|
actor_id: userId ?? "",
|
|
emoji: vars.emoji,
|
|
created_at: "",
|
|
},
|
|
];
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}, [serverReactions, pendingVars, issueId, userId]);
|
|
|
|
// --- Mutation ---
|
|
|
|
const toggleReaction = useCallback(
|
|
async (emoji: string) => {
|
|
if (!userId) return;
|
|
const existing = serverReactions.find(
|
|
(r) =>
|
|
r.emoji === emoji &&
|
|
r.actor_type === "member" &&
|
|
r.actor_id === userId,
|
|
);
|
|
toggleMutation.mutate({ emoji, existing });
|
|
},
|
|
[userId, serverReactions, toggleMutation],
|
|
);
|
|
|
|
return { reactions, loading, toggleReaction };
|
|
}
|