import { useCallback, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useCopilot } from '../../context/copilot.context'; import { resolveInPageTarget } from './in-page-routes'; /** The views that can sit beside the conversation. */ export type CanvasView = 'week' | 'content' | 'catalog' | 'templates'; /** Where the open canvas is written down: the address, so a canvas the * merchant opened survives a reload and can be linked to. */ const VIEW_PARAM = 'view'; /** What is open beside the conversation, and the ways to change it. */ export interface HomeCanvas { /** The view open in the canvas, if any. */ view: CanvasView | null; /** Something is open beside the conversation. */ hasCanvas: boolean; openView: (view: CanvasView) => void; close: () => void; /** Open a plugin route beside the thread; false when it has no canvas and * the caller should navigate instead. */ openInPage: (path: string) => boolean; } /** The views, so an address nobody wrote cannot open a canvas that is not there. */ const VIEWS: ReadonlySet = new Set([ 'week', 'content', 'catalog', 'templates', ]); /** * What is open beside the conversation, read from the address rather than * from state: opening a canvas must not tear the thread down, and a canvas * the merchant left open should still be open when they come back. * * @return The canvas and the ways to change it. */ export function useHomeCanvas(): HomeCanvas { const copilot = useCopilot(); const [params, setParams] = useSearchParams(); const raw: string = params.get(VIEW_PARAM) ?? ''; const view: CanvasView | null = VIEWS.has(raw) ? (raw as CanvasView) : null; const openView = useCallback( (next: CanvasView): void => setParams({ [VIEW_PARAM]: next }), [setParams] ); const close = useCallback((): void => setParams({}), [setParams]); const openInPage = useCallback( (path: string): boolean => { const target = resolveInPageTarget(path); if (target.kind !== 'canvas') return false; openView(target.view); return true; }, [openView] ); // Registered for the whole surface, not just the conversation: a card in // the thread, a chip under a reply and a directive arriving with no // component at all are three levels apart and need the same answer. const setOpenInPage = copilot.setOpenInPage; useEffect(() => { setOpenInPage(openInPage); return () => setOpenInPage(null); }, [setOpenInPage, openInPage]); return { view, hasCanvas: view !== null, openView, close, openInPage }; }