feat(offline): PWA shell, Yjs-backed titles, and offline read cache (M0–M2)
Implements docs/offline-sync-plan.md milestones M0–M2. M0 (PWA shell): - Add vite-plugin-pwa (generateSW, registerType: 'prompt', manifest:false); NetworkOnly for /api,/collab,/socket.io, NetworkFirst for GET /api, navigateFallback to index.html. - Register SW via useRegisterSW with a Mantine update prompt; skip registration inside Capacitor native WebView (is-capacitor guard). M1 (harden CRDT body + title into Yjs): - Lift the per-page Y.Doc/Hocuspocus providers into a shared hook+context so body and title editors share one doc. - Move the page title into a dedicated 'title' Yjs fragment (CRDT, offline- tolerant); drop the REST title save. Server persists the title fragment to page.title and seeds it for legacy pages (empty-fragment guard); a collab rename emits a treeUpdate so other users' tree/breadcrumbs refresh. - Persist the rebuilt ydoc on the content->ydoc path to neutralize the Yjs duplication trap. Add a 3-state sync indicator. M2 (offline read/navigation): - Persist React Query to IndexedDB (idb-keyval persister, version buster, selected roots only). - "Make available offline" action warms page, space, tree (root+ancestors+ children) and comments under exact hook keys, plus the page ydoc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { useDisclosure } from "@mantine/hooks";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconCloudDownload,
|
||||
IconCopy,
|
||||
IconDotsVertical,
|
||||
IconFileExport,
|
||||
@@ -32,6 +33,12 @@ import {
|
||||
} from "@/features/favorite/queries/favorite-query";
|
||||
|
||||
import { useToggleTemplateMutation } from "@/features/page-embed/queries/page-embed-query";
|
||||
import { useCollabToken } from "@/features/auth/queries/auth-query.tsx";
|
||||
import { getCollaborationUrl } from "@/lib/config.ts";
|
||||
import {
|
||||
makePageAvailableOffline,
|
||||
warmPageYdoc,
|
||||
} from "@/features/offline/make-offline";
|
||||
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
|
||||
import { treeModel } from "@/features/page/tree/model/tree-model";
|
||||
import { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.ts";
|
||||
@@ -67,6 +74,29 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) {
|
||||
const isFavorited = favoriteIds.has(node.id);
|
||||
const toggleTemplate = useToggleTemplateMutation();
|
||||
const isTemplate = !!node.isTemplate;
|
||||
const { data: collabQuery } = useCollabToken();
|
||||
|
||||
const handleMakeAvailableOffline = async () => {
|
||||
notifications.show({ message: t("Saving page for offline use...") });
|
||||
try {
|
||||
// Prefetch read queries so they get persisted to IndexedDB.
|
||||
await makePageAvailableOffline({
|
||||
pageId: node.id,
|
||||
slugId: node.slugId,
|
||||
spaceId: node.spaceId,
|
||||
parentPageId: node.parentPageId,
|
||||
});
|
||||
// Best-effort: warm the page's Yjs document into IndexedDB.
|
||||
await warmPageYdoc(node.id, getCollaborationUrl(), collabQuery?.token);
|
||||
notifications.show({ message: t("Page is now available offline") });
|
||||
} catch {
|
||||
// makePageAvailableOffline / warmPageYdoc never throw, but stay safe.
|
||||
notifications.show({
|
||||
message: t("Failed to make page available offline"),
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTemplate = async () => {
|
||||
const next = !isTemplate;
|
||||
@@ -204,6 +234,17 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) {
|
||||
{t("Export page")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
leftSection={<IconCloudDownload size={16} />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleMakeAvailableOffline();
|
||||
}}
|
||||
>
|
||||
{t("Make available offline")}
|
||||
</Menu.Item>
|
||||
|
||||
{canEdit && (
|
||||
<>
|
||||
<Menu.Item
|
||||
|
||||
Reference in New Issue
Block a user