"use client"; /** * CommentPanel — the page's comment drawer (TASK-0094 R2). * * Mounts the Phase-3 `CommentThread` (the thread list + composer, already * TanStack-backed) and wires it to realtime: * - on a `comment.event` frame for this page (the room's fan-out, R1): * created / replied → bump the "New comments (n)" pill (when the * panel is closed) + invalidate the thread list (a refetch is the * authoritative state — the frame carries only a `preview`), * resolved/reopened/reacted/deleted → invalidate (a state change on an * existing thread, not a new comment — the pill is untouched), * - opening the panel clears the pill (the refetch shows the new comments) * + refetches (a fresh mount re-reads the server, the source of truth). * * The panel is REMOTE-update only: the local save path (the composer's * mutation) is already optimistic via the thread list; the frame reconciles * it. The 300 ms budget (R2) is REST write → api publish → stream → this * frame → refetch — the frame is the fast path, the refetch is lossless. */ import { useEffect } from "react"; import { X } from "lucide-react"; import { useQueryClient } from "@tanstack/react-query"; import { useWorkspaceMembers } from "@/hooks/use-workspace"; import { useAuthStore } from "@/lib/auth-store"; import { realtime } from "@/lib/realtime"; import { useCommentPillStore } from "@/stores/comment-pill-store"; import { CommentThread, type CommentAnchor } from "./comment-thread"; export interface CommentPanelProps { pageId: string; workspaceId: string; open: boolean; onClose: () => void; } export function CommentPanel({ pageId, workspaceId, open, onClose }: CommentPanelProps) { const qc = useQueryClient(); const currentUserId = useAuthStore((s) => s.user?.id ?? null); const { data: members = [] } = useWorkspaceMembers(workspaceId); const clearPill = useCommentPillStore((s) => s.clear); // The composer is enabled for workspace members (the server re-authorizes // COMMENT — this is UX only; a shared viewer without a membership is // view-only). const canComment = members.some((m) => m.userId === currentUserId); const composerMembers = members.map((m) => m.user); // Realtime: fold this page's `comment.event` frames into the thread list. // The socket + the `page:` channel are already open (the page layout's // usePageRealtime) — the panel reuses the same connection (R2). The "New // comments" pill is owned by the page header (it bumps only while this // panel is CLOSED) — here every frame is a thread change → refetch. useEffect(() => { if (!open) return; const off = realtime.on("comment.event", (frame) => { if (frame.type !== "comment.event" || frame.pageId !== pageId) return; // The frame is a hint (a `preview`); the refetch is authoritative + // lossless (correct author/timestamps/reactions). void qc.invalidateQueries({ queryKey: ["comments", pageId] }); }); return () => off(); }, [open, pageId, qc]); // Opening the panel: clear the pill (a refetch shows the new comments) + // a fresh read of the thread list. useEffect(() => { if (open) { clearPill(pageId); void qc.invalidateQueries({ queryKey: ["comments", pageId] }); } }, [open, pageId, qc, clearPill]); if (!open) return null; const anchor: CommentAnchor = { pageId }; return ( <> {/* Backdrop (a click closes the panel). */}
); }