/** * @file usePanePresence.ts * @description Who has each of a workspace's panes open (server mode, workspace * sharing phase 4): `GET /api/lanes/:id/presence` once, then every * `pane_presence` frame for that workspace, and a re-read on a WebSocket * reconnect (frames missed while away are never replayed). Local mode has * one user and reads nothing. Returns `{runId: [{id, name}]}`. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { api } from "../lib/api"; import { eventBus } from "../lib/eventBus"; import { usePermissions } from "../components/AuthGate"; export type PanePeople = Record; export function usePanePresence(laneId: number | null | undefined): PanePeople { const { local } = usePermissions(); const [panes, setPanes] = useState({}); useEffect(() => { setPanes({}); if (local || laneId == null) return; let alive = true; const load = () => Promise.resolve() .then(() => api.lanes.presence(laneId)) .then((r) => alive && setPanes(r.panes)) .catch(() => { /* presence is a nicety: no list, no chips */ }); void load(); const offMsg = eventBus.subscribe((msg) => { if (msg.type !== "pane_presence") return; const d = msg.data as { laneId?: number; runId?: string; users?: PanePeople[string] }; if (d.laneId !== laneId || !d.runId) return; setPanes((prev) => { const next = { ...prev }; if (d.users?.length) next[d.runId!] = d.users; else delete next[d.runId!]; return next; }); }); const offConn = eventBus.onConnection((up) => { if (up) void load(); }); return () => { alive = false; offMsg(); offConn(); }; }, [laneId, local]); return panes; }