- Add getQueryClient() singleton for non-React contexts (WS handlers, Zustand) - Create issue query key factory + 5 queryOptions - Create 11 mutation hooks with optimistic updates and rollback - Create WS cache updaters + dual-write in use-realtime-sync - Rewrite useIssueTimeline, useIssueReactions, useIssueSubscribers to TQ (return types unchanged, consumers unaffected) - Add QueryClientProvider wrapper to issue detail tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
116 lines
3 KiB
TypeScript
116 lines
3 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback } from "react";
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import type { IssueSubscriber } from "@/shared/types";
|
|
import type {
|
|
SubscriberAddedPayload,
|
|
SubscriberRemovedPayload,
|
|
} from "@/shared/types";
|
|
import { issueSubscribersOptions, issueKeys } from "@core/issues/queries";
|
|
import { useToggleIssueSubscriber } from "@core/issues/mutations";
|
|
import { useWSEvent, useWSReconnect } from "@/features/realtime";
|
|
|
|
export function useIssueSubscribers(issueId: string, userId?: string) {
|
|
const qc = useQueryClient();
|
|
const { data: subscribers = [], isLoading: loading } = useQuery(
|
|
issueSubscribersOptions(issueId),
|
|
);
|
|
|
|
const toggleMutation = useToggleIssueSubscriber(issueId);
|
|
|
|
// Reconnect recovery
|
|
useWSReconnect(
|
|
useCallback(() => {
|
|
qc.invalidateQueries({ queryKey: issueKeys.subscribers(issueId) });
|
|
}, [qc, issueId]),
|
|
);
|
|
|
|
// --- WS event handlers ---
|
|
|
|
useWSEvent(
|
|
"subscriber:added",
|
|
useCallback(
|
|
(payload: unknown) => {
|
|
const p = payload as SubscriberAddedPayload;
|
|
if (p.issue_id !== issueId) return;
|
|
qc.setQueryData<IssueSubscriber[]>(
|
|
issueKeys.subscribers(issueId),
|
|
(old) => {
|
|
if (!old) return old;
|
|
if (
|
|
old.some(
|
|
(s) =>
|
|
s.user_id === p.user_id && s.user_type === p.user_type,
|
|
)
|
|
)
|
|
return old;
|
|
return [
|
|
...old,
|
|
{
|
|
issue_id: p.issue_id,
|
|
user_type: p.user_type as "member" | "agent",
|
|
user_id: p.user_id,
|
|
reason: p.reason as IssueSubscriber["reason"],
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
];
|
|
},
|
|
);
|
|
},
|
|
[qc, issueId],
|
|
),
|
|
);
|
|
|
|
useWSEvent(
|
|
"subscriber:removed",
|
|
useCallback(
|
|
(payload: unknown) => {
|
|
const p = payload as SubscriberRemovedPayload;
|
|
if (p.issue_id !== issueId) return;
|
|
qc.setQueryData<IssueSubscriber[]>(
|
|
issueKeys.subscribers(issueId),
|
|
(old) =>
|
|
old?.filter(
|
|
(s) =>
|
|
!(s.user_id === p.user_id && s.user_type === p.user_type),
|
|
),
|
|
);
|
|
},
|
|
[qc, issueId],
|
|
),
|
|
);
|
|
|
|
// --- Mutations ---
|
|
|
|
const isSubscribed = subscribers.some(
|
|
(s) => s.user_type === "member" && s.user_id === userId,
|
|
);
|
|
|
|
const toggleSubscriber = useCallback(
|
|
async (
|
|
subUserId: string,
|
|
userType: "member" | "agent",
|
|
currentlySubscribed: boolean,
|
|
) => {
|
|
toggleMutation.mutate({
|
|
userId: subUserId,
|
|
userType,
|
|
subscribed: currentlySubscribed,
|
|
});
|
|
},
|
|
[toggleMutation],
|
|
);
|
|
|
|
const toggleSubscribe = useCallback(() => {
|
|
if (userId) toggleSubscriber(userId, "member", isSubscribed);
|
|
}, [userId, isSubscribed, toggleSubscriber]);
|
|
|
|
return {
|
|
subscribers,
|
|
loading,
|
|
isSubscribed,
|
|
toggleSubscribe,
|
|
toggleSubscriber,
|
|
};
|
|
}
|