File
Blob: src/client/components/active-page/shared.tsx
| 1 | import { useEffect, type ReactNode } from "react"; |
| 2 | import { SharedPageMentionSurface } from "@/client/components/page-mention/shared-surface"; |
| 3 | import { ActivePageProvider } from "@/client/components/active-page/provider"; |
| 4 | import { useActivePageActions, useActivePageSync } from "@/client/components/active-page/use-active-page"; |
| 5 | import { useReadyShareView } from "@/client/components/share/use-share-view"; |
| 6 | import type { ActivePageInitialSnapshot } from "@/client/lib/active-page-model"; |
| 7 | import { parseDocMessage } from "@/shared/doc-messages"; |
| 8 | |
| 9 | /** |
| 10 | * Share boundary: keeps share-link resolution token-scoped in ShareViewProvider |
| 11 | * and mounts active-page state only for the active shared page. The |
| 12 | * `key={activePageId}` forces a remount on subpage↔root transitions so the |
| 13 | * `initialSnapshot` seed for the root page lands via the useState initializer |
| 14 | * rather than an effect — no intermediate paint with empty metadata. |
| 15 | */ |
| 16 | export function SharedActivePageBoundary({ children }: { children: ReactNode }) { |
| 17 | const { token, workspaceId, rootPageId, rootPage, activePageId } = useReadyShareView(); |
| 18 | |
| 19 | const initialSnapshot: ActivePageInitialSnapshot | null = |
| 20 | activePageId === rootPageId |
| 21 | ? { |
| 22 | snapshot: { |
| 23 | id: rootPage.id, |
| 24 | workspaceId, |
| 25 | kind: rootPage.kind, |
| 26 | title: rootPage.title, |
| 27 | icon: rootPage.icon, |
| 28 | coverUrl: rootPage.cover_url, |
| 29 | }, |
| 30 | access: { mode: rootPage.permission }, |
| 31 | } |
| 32 | : null; |
| 33 | |
| 34 | return ( |
| 35 | <ActivePageProvider |
| 36 | key={activePageId} |
| 37 | surface="shared" |
| 38 | workspaceId={workspaceId} |
| 39 | pageId={activePageId} |
| 40 | accessMode="shared" |
| 41 | role={null} |
| 42 | cachedPageMeta={null} |
| 43 | cachedAccess={null} |
| 44 | shareToken={token} |
| 45 | initialSnapshot={initialSnapshot} |
| 46 | > |
| 47 | <SharedMetadataListener /> |
| 48 | <SharedPageMentionSurface>{children}</SharedPageMentionSurface> |
| 49 | </ActivePageProvider> |
| 50 | ); |
| 51 | } |
| 52 | |
| 53 | function SharedMetadataListener() { |
| 54 | const { syncProvider } = useActivePageSync(); |
| 55 | const { patchPage } = useActivePageActions(); |
| 56 | |
| 57 | useEffect(() => { |
| 58 | if (!syncProvider) return; |
| 59 | const handler = (message: string) => { |
| 60 | const msg = parseDocMessage(message); |
| 61 | if (msg?.type === "page-metadata-updated") { |
| 62 | patchPage({ icon: msg.icon, coverUrl: msg.cover_url }); |
| 63 | } |
| 64 | }; |
| 65 | syncProvider.on("custom-message", handler); |
| 66 | return () => syncProvider.off("custom-message", handler); |
| 67 | }, [syncProvider, patchPage]); |
| 68 | |
| 69 | return null; |
| 70 | } |