Files
kandev/apps/web/hooks/use-panel-active.ts
T
2026-09-07 21:17:02 +00:00

73 lines
3.3 KiB
TypeScript

"use client";
import { useCallback, useSyncExternalStore } from "react";
import type { DockviewPanelApi } from "dockview-react";
import { panelPortalManager } from "@/lib/layout/panel-portal-manager";
/**
* Tracks whether the dockview panel identified by panelId is the visible tab
* in its dockview group. Portal-hosted panel content (see
* `lib/layout/panel-portal-manager.ts`) stays mounted while its tab is
* inactive — e.g. the Chat tab keeps running behind an active Files/Changes
* tab — so components that must distinguish "mounted" from "actually on
* screen" (like the unread-divider read tracker) need this signal rather
* than assuming mount implies visibility.
*
* Backed by `panelPortalManager` since portal-hosted content only receives
* its `panelId`, not the dockview api directly; `usePortalSlot`'s mount
* effect acquires the portal entry (with its api) before any child content
* renders, so the entry is virtually always present by the time this runs.
* Subscribes to both the manager's own add/remove notifications (a panel
* acquired after this hook's first mount, or released while it stays
* mounted) and the panel's own `onDidVisibilityChange` (tab switches within
* an existing group). Dockview's `isActive` also requires the panel's group
* to own global focus, so it is not a valid proxy for whether the panel is
* rendered on screen beside another focused group.
*
* Only for dockview-hosted panels — pass the panel's real id. Defaults to
* `false` until a portal entry/api is actually registered for it, rather
* than assuming visibility during the brief acquire/mount race or after a
* panel has been detached: a false negative just delays marking the session
* read by a render, while a false positive could mark it read too early.
* Non-dockview hosts (mobile, quick chat, kanban preview) never acquire a
* portal entry — they should pass a literal `true`/`false` instead of
* calling this hook.
*/
export function usePanelActive(panelId: string): boolean {
const subscribe = useCallback(
(onStoreChange: () => void) => {
// Tracks which api our onDidVisibilityChange listener is currently bound
// to, so a manager notification (panel acquired/released/remounted
// anywhere) can detect when *this* panelId's api was replaced —
// Dockview does this during a layout restore/switch — and rebind
// rather than keep listening on the now-stale api.
let boundApi: DockviewPanelApi | null = null;
let disposable: { dispose: () => void } | null = null;
const rebindIfApiChanged = () => {
const api = panelPortalManager.get(panelId)?.api ?? null;
if (api === boundApi) return;
disposable?.dispose();
boundApi = api;
disposable = api?.onDidVisibilityChange(onStoreChange) ?? null;
};
rebindIfApiChanged();
const unsubscribeManager = panelPortalManager.subscribe(() => {
rebindIfApiChanged();
onStoreChange();
});
return () => {
unsubscribeManager();
disposable?.dispose();
};
},
[panelId],
);
const getSnapshot = useCallback(
() => panelPortalManager.get(panelId)?.api?.isVisible ?? false,
[panelId],
);
return useSyncExternalStore(subscribe, getSnapshot, () => false);
}