multica/apps/web/features/issues/hooks/use-issue-subscribers.ts
Naiyuan Qing 7560f7be85 feat(core/issues): add TanStack Query layer and rewrite hooks (Phase 1, Commits 1-4)
- 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>
2026-04-07 15:30:42 +08:00

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,
};
}