Skip to content
File

Blob: src/client/components/active-page/canonical.tsx

typescript81 lines
1import { useEffect, type ReactNode } from "react";
2import { useParams } from "@tanstack/react-router";
3import { CanonicalPageMentionSurface } from "@/client/components/page-mention/canonical-surface";
4import { usePageAccessMode } from "@/client/stores/workspace-replica";
5import { replicaCommands } from "@/client/stores/db/workspace-replica";
6import { useCanonicalPageContext } from "@/client/components/workspace/use-canonical-page-context";
7import { ActivePageProvider } from "@/client/components/active-page/provider";
8import { useActivePageActions, useActivePageSync } from "@/client/components/active-page/use-active-page";
9import { parseDocMessage } from "@/shared/doc-messages";
10import type { ActivePageAccess } from "@/client/lib/active-page-model";
11import type { Page } from "@/shared/types";
12 
13/**
14 * Canonical boundary: mounts the shared ActivePageProvider with canonical
15 * inputs, wires replica mutators as cache side-effects, and listens for
16 * real-time metadata updates on the active doc's WebSocket.
17 */
18export function CanonicalActivePageBoundary({ children }: { children: ReactNode }) {
19 const params = useParams({ strict: false }) as { workspaceSlug: string; pageId: string };
20 const { workspaceId: effectiveWorkspaceId, accessMode, workspaceRole, currentPageMeta } = useCanonicalPageContext();
21 // Role flows from the workspace replica so restricted-ancestor loading does
22 // not depend on the `/members` fetch (which is empty on shared-surface
23 // replicas and scoped to self on guest surfaces).
24 const role = workspaceRole;
25 
26 const cachedAccess = usePageAccessMode(params.pageId);
27 
28 const onLivePageLoaded = (page: Page, access: ActivePageAccess) => {
29 if (!effectiveWorkspaceId) return;
30 void replicaCommands.upsertPage(effectiveWorkspaceId, page);
31 void replicaCommands.upsertPageAccess(page.id, access.mode);
32 };
33 
34 const onEvict = (id: string) => {
35 if (!effectiveWorkspaceId) return;
36 void replicaCommands.removePage(effectiveWorkspaceId, id);
37 };
38 
39 return (
40 <ActivePageProvider
41 surface="canonical"
42 workspaceId={effectiveWorkspaceId}
43 pageId={params.pageId}
44 accessMode={accessMode}
45 role={role}
46 cachedPageMeta={currentPageMeta}
47 cachedAccess={cachedAccess}
48 shareToken={null}
49 onLivePageLoaded={onLivePageLoaded}
50 onEvict={onEvict}
51 >
52 <CanonicalMetadataListener />
53 <CanonicalPageMentionSurface>{children}</CanonicalPageMentionSurface>
54 </ActivePageProvider>
55 );
56}
57 
58function CanonicalMetadataListener() {
59 const { syncProvider } = useActivePageSync();
60 const { patchPage } = useActivePageActions();
61 const { workspace } = useCanonicalPageContext();
62 const workspaceId = workspace?.id ?? null;
63 
64 useEffect(() => {
65 if (!syncProvider || !workspaceId) return;
66 const handler = (message: string) => {
67 const msg = parseDocMessage(message);
68 if (msg?.type === "page-metadata-updated") {
69 patchPage({ icon: msg.icon, coverUrl: msg.cover_url });
70 void replicaCommands.patchPage(workspaceId, msg.pageId, {
71 icon: msg.icon,
72 cover_url: msg.cover_url,
73 });
74 }
75 };
76 syncProvider.on("custom-message", handler);
77 return () => syncProvider.off("custom-message", handler);
78 }, [patchPage, syncProvider, workspaceId]);
79 return null;
80}