From ecdb5b3407d02bd5698d146a2abd365aae347c68 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Tue, 23 Jan 2024 01:41:44 +0000 Subject: [PATCH] fix(core): properties adapter reactivitiy issue (#5661) --- .../affine/page-history-modal/data.ts | 23 +++++++++----- .../page-history-modal/history-modal.tsx | 8 ++--- ...-block-suite-workspace-page-title.spec.tsx | 2 ++ .../core/src/hooks/use-affine-adapter.ts | 20 ++++++------ .../src/modules/workspace/properties/atom.ts | 31 +++++++++++-------- .../src/pages/share/share-detail-page.tsx | 1 + 6 files changed, 51 insertions(+), 34 deletions(-) diff --git a/packages/frontend/core/src/components/affine/page-history-modal/data.ts b/packages/frontend/core/src/components/affine/page-history-modal/data.ts index 3fbde19f9..334731e12 100644 --- a/packages/frontend/core/src/components/affine/page-history-modal/data.ts +++ b/packages/frontend/core/src/components/affine/page-history-modal/data.ts @@ -13,9 +13,9 @@ import { createAffineCloudBlobStorage } from '@affine/workspace-impl'; import { assertEquals } from '@blocksuite/global/utils'; import { Workspace } from '@blocksuite/store'; import { revertUpdate } from '@toeverything/y-indexeddb'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import useSWRImmutable from 'swr/immutable'; -import { applyUpdate } from 'yjs'; +import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { useMutateQueryResource, @@ -105,7 +105,7 @@ const snapshotFetcher = async ( const workspaceMap = new Map(); // assume the workspace is a cloud workspace since the history feature is only enabled for cloud workspace -const getOrCreateWorkspace = (workspaceId: string) => { +const getOrCreateShellWorkspace = (workspaceId: string) => { let workspace = workspaceMap.get(workspaceId); if (!workspace) { const blobStorage = createAffineCloudBlobStorage(workspaceId); @@ -120,6 +120,7 @@ const getOrCreateWorkspace = (workspaceId: string) => { schema: globalBlockSuiteSchema, }); workspaceMap.set(workspaceId, workspace); + workspace.doc.emit('sync', []); } return workspace; }; @@ -143,17 +144,17 @@ export const usePageHistory = ( // workspace id + page id + timestamp + snapshot -> Page (to be used for rendering in blocksuite editor) export const useSnapshotPage = ( - workspaceId: string, + workspace: Workspace, pageDocId: string, ts?: string ) => { - const snapshot = usePageHistory(workspaceId, pageDocId, ts); + const snapshot = usePageHistory(workspace.id, pageDocId, ts); const page = useMemo(() => { if (!ts) { return; } const pageId = pageDocId + '-' + ts; - const historyShellWorkspace = getOrCreateWorkspace(workspaceId); + const historyShellWorkspace = getOrCreateShellWorkspace(workspace.id); let page = historyShellWorkspace.getPage(pageId); if (!page && snapshot) { page = historyShellWorkspace.createPage({ @@ -169,7 +170,15 @@ export const useSnapshotPage = ( .catch(console.error); // must load before applyUpdate } return page ?? undefined; - }, [pageDocId, snapshot, ts, workspaceId]); + }, [pageDocId, snapshot, ts, workspace]); + + useEffect(() => { + const historyShellWorkspace = getOrCreateShellWorkspace(workspace.id); + // apply the rootdoc's update to the current workspace + // this makes sure the page reference links are not deleted ones in the preview + const update = encodeStateAsUpdate(workspace.doc); + applyUpdate(historyShellWorkspace.doc, update); + }, [workspace]); return page; }; diff --git a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx index abfb7ee98..777fd20c0 100644 --- a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx +++ b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx @@ -5,6 +5,7 @@ import { ConfirmModal, Modal } from '@affine/component/ui/modal'; import { openSettingModalAtom, type PageMode } from '@affine/core/atoms'; import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title'; import { useUserSubscription } from '@affine/core/hooks/use-subscription'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { timestampToLocalTime } from '@affine/core/utils'; @@ -418,7 +419,7 @@ const PageHistoryManager = ({ return workspace.getPage(pageId)?.spaceDoc.guid ?? pageId; }, [pageId, workspace]); - const snapshotPage = useSnapshotPage(workspaceId, pageDocId, activeVersion); + const snapshotPage = useSnapshotPage(workspace, pageDocId, activeVersion); const t = useAFFiNEI18N(); @@ -440,10 +441,7 @@ const PageHistoryManager = ({ const defaultPreviewPageMode = useAtomValue(currentModeAtom); const [mode, setMode] = useState(defaultPreviewPageMode); - const title = useMemo( - () => workspace.getPage(pageId)?.meta.title || t['Untitled'](), - [pageId, t, workspace] - ); + const title = useBlockSuiteWorkspacePageTitle(workspace, pageId); const [showRestoreConfirmModal, setShowRestoreConfirmModal] = useState(false); diff --git a/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx b/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx index c7d436580..8f5eaeb86 100644 --- a/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx +++ b/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx @@ -51,6 +51,8 @@ beforeEach(async () => { blockSuiteWorkspace = workspace.blockSuiteWorkspace; + blockSuiteWorkspace.doc.emit('sync', []); + const initPage = async (page: Page) => { await page.waitForLoaded(); expect(page).not.toBeNull(); diff --git a/packages/frontend/core/src/hooks/use-affine-adapter.ts b/packages/frontend/core/src/hooks/use-affine-adapter.ts index b55a92e99..00efa2fe3 100644 --- a/packages/frontend/core/src/hooks/use-affine-adapter.ts +++ b/packages/frontend/core/src/hooks/use-affine-adapter.ts @@ -1,20 +1,25 @@ import type { Workspace } from '@blocksuite/store'; import { useAtomValue } from 'jotai'; -import { useEffect, useMemo, useReducer } from 'react'; +import { useEffect, useState } from 'react'; +import type { WorkspacePropertiesAdapter } from '../modules/workspace/properties'; import { currentWorkspacePropertiesAdapterAtom, - WorkspacePropertiesAdapter, + workspaceAdapterAtomFamily, } from '../modules/workspace/properties'; +function getProxy(obj: T) { + return new Proxy(obj, {}); +} + const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => { - const [, forceUpdate] = useReducer(c => c + 1, 0); + const [proxy, setProxy] = useState(adapter); useEffect(() => { // todo: track which properties are used and then filter by property path change // using Y.YEvent.path function observe() { - forceUpdate(); + setProxy(getProxy(adapter)); } adapter.properties.observeDeep(observe); return () => { @@ -22,7 +27,7 @@ const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => { }; }, [adapter]); - return adapter; + return proxy; }; export function useCurrentWorkspacePropertiesAdapter() { @@ -31,9 +36,6 @@ export function useCurrentWorkspacePropertiesAdapter() { } export function useWorkspacePropertiesAdapter(workspace: Workspace) { - const adapter = useMemo( - () => new WorkspacePropertiesAdapter(workspace), - [workspace] - ); + const adapter = useAtomValue(workspaceAdapterAtomFamily(workspace)); return useReactiveAdapter(adapter); } diff --git a/packages/frontend/core/src/modules/workspace/properties/atom.ts b/packages/frontend/core/src/modules/workspace/properties/atom.ts index 30db075d7..df4e0da47 100644 --- a/packages/frontend/core/src/modules/workspace/properties/atom.ts +++ b/packages/frontend/core/src/modules/workspace/properties/atom.ts @@ -1,16 +1,21 @@ -import type { Workspace } from '@affine/workspace/workspace'; -import { atomWithObservable } from 'jotai/utils'; -import { filter, map, of } from 'rxjs'; +import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; +import { atom } from 'jotai'; +import { atomFamily } from 'jotai/utils'; -import { currentWorkspaceAtom } from '../atoms'; +import { waitForCurrentWorkspaceAtom } from '../atoms'; import { WorkspacePropertiesAdapter } from './adapter'; -export const currentWorkspacePropertiesAdapterAtom = - atomWithObservable(get => { - return of(get(currentWorkspaceAtom)).pipe( - filter((workspace): workspace is Workspace => !!workspace), - map(workspace => { - return new WorkspacePropertiesAdapter(workspace.blockSuiteWorkspace); - }) - ); - }); +// todo: remove the inner atom when workspace is closed by using workspaceAdapterAtomFamily.remove +export const workspaceAdapterAtomFamily = atomFamily( + (workspace: BlockSuiteWorkspace) => { + return atom(async () => { + await workspace.doc.whenLoaded; + return new WorkspacePropertiesAdapter(workspace); + }); + } +); + +export const currentWorkspacePropertiesAdapterAtom = atom(async get => { + const workspace = await get(waitForCurrentWorkspaceAtom); + return get(workspaceAdapterAtomFamily(workspace.blockSuiteWorkspace)); +}); diff --git a/packages/frontend/core/src/pages/share/share-detail-page.tsx b/packages/frontend/core/src/pages/share/share-detail-page.tsx index 4c1143957..f3b1661d8 100644 --- a/packages/frontend/core/src/pages/share/share-detail-page.tsx +++ b/packages/frontend/core/src/pages/share/share-detail-page.tsx @@ -99,6 +99,7 @@ export const loader: LoaderFunction = async ({ params }) => { assertDownloadResponse(response); const { arrayBuffer } = response; applyUpdate(workspace.doc, new Uint8Array(arrayBuffer)); + workspace.doc.emit('sync', []); } const page = workspace.getPage(pageId); assertExists(page, 'cannot find page');