Files
multica/packages/views/issues/hooks/use-issue-subscribers.ts
Naiyuan Qing f41a0cf423 feat(views): extract packages/views — shared business UI + navigation adapter
- Create NavigationAdapter interface (push, replace, back, pathname, searchParams)
- Create AppLink component replacing next/link in 4 files
- Replace useRouter → useNavigation in 3 files (issue-detail, create-issue, create-workspace)
- Create WebNavigationProvider wrapping Next.js useRouter/usePathname/useSearchParams
- Move ~85 feature UI files (issues, editor, modals, my-issues, skills, runtimes) to packages/views/
- Add store singleton registration pattern (registerAuthStore, registerWorkspaceStore)
- Create data-aware wrappers in packages/views/common/ (ActorAvatar, Markdown)
- Update all app-layer imports to @multica/views/*
- Add @source directive for Tailwind to scan views package
- packages/views/ has zero next/* imports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:49:55 +08:00

117 lines
3.0 KiB
TypeScript

"use client";
import { useCallback } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import type { IssueSubscriber } from "@multica/core/types";
import type {
SubscriberAddedPayload,
SubscriberRemovedPayload,
} from "@multica/core/types";
import { issueSubscribersOptions, issueKeys } from "@multica/core/issues/queries";
import { useToggleIssueSubscriber } from "@multica/core/issues/mutations";
import { useWSEvent, useWSReconnect } from "@multica/core/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,
};
}