From fba0cb9015a1900150bc74147b66055339f3d35b Mon Sep 17 00:00:00 2001 From: alt0 Date: Tue, 20 Dec 2022 00:04:33 +0800 Subject: [PATCH] feat: normalize load page --- .../app/src/components/header/sync-icon.tsx | 2 +- .../SelectorPopperContent.tsx | 2 +- .../WorkspaceItem/PrivateWorkspaceItem.tsx | 2 +- packages/app/src/pages/_app.tsx | 2 +- .../workspace/[workspaceId]/[pageId].tsx | 48 ++++++- .../pages/workspace/[workspaceId]/index.tsx | 28 ++++ .../app/src/providers/app-state-provider.tsx | 57 -------- .../providers/app-state-provider/context.ts | 49 +++++++ .../app-state-provider/dynamic-blocksuite.tsx | 76 +++++++++++ .../src/providers/app-state-provider/hooks.ts | 30 +++++ .../providers/app-state-provider/provider.tsx | 125 ++++++++++++++++++ .../editor-provider/editor-provider.tsx | 2 +- .../editor-provider/editor-reactor.tsx | 3 +- 13 files changed, 359 insertions(+), 67 deletions(-) delete mode 100644 packages/app/src/providers/app-state-provider.tsx create mode 100644 packages/app/src/providers/app-state-provider/context.ts create mode 100644 packages/app/src/providers/app-state-provider/dynamic-blocksuite.tsx create mode 100644 packages/app/src/providers/app-state-provider/hooks.ts create mode 100644 packages/app/src/providers/app-state-provider/provider.tsx diff --git a/packages/app/src/components/header/sync-icon.tsx b/packages/app/src/components/header/sync-icon.tsx index 618ee7b37d..3d791c0056 100644 --- a/packages/app/src/components/header/sync-icon.tsx +++ b/packages/app/src/components/header/sync-icon.tsx @@ -1,6 +1,6 @@ import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons'; import { useModal } from '@/providers/global-modal-provider'; -import { useAppState } from '@/providers/app-state-provider'; +import { useAppState } from '@/providers/app-state-provider/context'; import { IconButton } from '@/ui/button'; export const SyncIcon = () => { diff --git a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent.tsx b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent.tsx index 30b7c8d162..2763c4db1a 100644 --- a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent.tsx +++ b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent.tsx @@ -1,7 +1,7 @@ import { InformationIcon, LogOutIcon } from '@blocksuite/icons'; import { styled } from '@/styles'; import { Divider } from '@/ui/divider'; -import { useAppState } from '@/providers/app-state-provider'; +import { useAppState } from '@/providers/app-state-provider/context'; import { SelectorPopperContainer } from './styles'; import { PrivateWorkspaceItem, diff --git a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/PrivateWorkspaceItem.tsx b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/PrivateWorkspaceItem.tsx index 791ac45e1f..33d1fffc89 100644 --- a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/PrivateWorkspaceItem.tsx +++ b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/PrivateWorkspaceItem.tsx @@ -1,5 +1,5 @@ import { styled } from '@/styles'; -import { useAppState } from '@/providers/app-state-provider'; +import { useAppState } from '@/providers/app-state-provider/context'; import { WorkspaceItemAvatar, PrivateWorkspaceWrapper, diff --git a/packages/app/src/pages/_app.tsx b/packages/app/src/pages/_app.tsx index 765e8324d8..647b390fca 100644 --- a/packages/app/src/pages/_app.tsx +++ b/packages/app/src/pages/_app.tsx @@ -5,7 +5,7 @@ import '../../public/variable.css'; import './temporary.css'; import { EditorProvider } from '@/providers/editor-provider'; import { ModalProvider } from '@/providers/global-modal-provider'; -import { AppStateProvider } from '@/providers/app-state-provider'; +import { AppStateProvider } from '@/providers/app-state-provider/provider'; import { Logger } from '@toeverything/pathfinder-logger'; import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar'; import '@fontsource/space-mono'; diff --git a/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx b/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx index fcc7770e04..3d0eab2fb0 100644 --- a/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx +++ b/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx @@ -1,9 +1,15 @@ +import { useRef, useEffect } from 'react'; import type { NextPage } from 'next'; import { styled } from '@/styles'; import { EditorHeader } from '@/components/header'; import EdgelessToolbar from '@/components/edgeless-toolbar'; import MobileModal from '@/components/mobile-modal'; -import Editor from '@/components/editor'; +import { + useLoadWorkspace, + useLoadPage, +} from '@/providers/app-state-provider/hooks'; +import { useAppState } from '@/providers/app-state-provider/context'; +import exampleMarkdown from '@/providers/editor-provider/example-markdown'; const StyledEditorContainer = styled('div')(({ theme }) => { return { @@ -12,13 +18,47 @@ const StyledEditorContainer = styled('div')(({ theme }) => { }); const Home: NextPage = () => { + const editorContainer = useRef(null); + const workspace = useLoadWorkspace(); + const page = useLoadPage(); + const { createEditor } = useAppState(); + + useEffect(() => { + const ret = () => { + const node = editorContainer.current; + while (node?.firstChild) { + node.removeChild(node.firstChild); + } + }; + + if (!workspace || !page) { + return ret; + } + + const editor = createEditor(page); + if (editor) { + editorContainer.current?.appendChild(editor); + if (page.isEmpty) { + const title = 'Welcome to the AFFiNE Alpha'; + const pageId = page.addBlock({ + flavour: 'affine:page', + title, + }); + const groupId = page.addBlock({ flavour: 'affine:group' }, pageId); + editor.clipboard.importMarkdown(exampleMarkdown, `${groupId}`); + workspace.setPageMeta(page.id, { title }); + page.resetHistory(); + } + } + + return ret; + }, [workspace, page]); + return ( <> - - - + ); diff --git a/packages/app/src/pages/workspace/[workspaceId]/index.tsx b/packages/app/src/pages/workspace/[workspaceId]/index.tsx index ef0a603c9e..46da75ea48 100644 --- a/packages/app/src/pages/workspace/[workspaceId]/index.tsx +++ b/packages/app/src/pages/workspace/[workspaceId]/index.tsx @@ -1,4 +1,32 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useLoadWorkspace } from '@/providers/app-state-provider/hooks'; +import { useAppState } from '@/providers/app-state-provider/context'; + const Page = () => { + const router = useRouter(); + const workspace = useLoadWorkspace(); + const { createPage, currentWorkspaceId } = useAppState(); + + useEffect(() => { + if (!workspace) { + return; + } + + const savedPageId = workspace.meta.pageMetas[0]?.id; + if (savedPageId) { + router.push(`/workspace/${currentWorkspaceId}/${savedPageId}`); + return; + } + + createPage()?.then(async pageId => { + if (!pageId) { + return; + } + router.push(`/workspace/${currentWorkspaceId}/${pageId}`); + }); + }, [workspace, currentWorkspaceId]); + return
Hello, there is [pageId]/index
; }; diff --git a/packages/app/src/providers/app-state-provider.tsx b/packages/app/src/providers/app-state-provider.tsx deleted file mode 100644 index 8627d9d41b..0000000000 --- a/packages/app/src/providers/app-state-provider.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { createContext, useMemo, useContext, useState, useEffect } from 'react'; -import type { ReactNode } from 'react'; -import { - authorizationEvent, - AccessTokenMessage, - getWorkspaces, -} from '@pathfinder/data-services'; -import type { Workspace } from '@pathfinder/data-services'; - -interface AppStateValue { - user: AccessTokenMessage | null; - workspacesMeta: Workspace[]; -} - -interface AppStateContext extends AppStateValue { - setState: (state: AppStateValue) => void; -} - -const AppState = createContext({ - user: null, - workspacesMeta: [], - setState: () => {}, -}); - -export const AppStateProvider = ({ children }: { children?: ReactNode }) => { - const [state, setState] = useState({ - user: null, - workspacesMeta: [], - }); - - useEffect(() => { - const callback = async (user: AccessTokenMessage | null) => { - const workspaces = user ? await getWorkspaces() : []; - setState(state => ({ ...state, user: user, workspaces })); - }; - authorizationEvent.onChange(callback); - - return () => { - authorizationEvent.removeCallback(callback); - }; - }, []); - - const context = useMemo( - () => ({ - ...state, - setState, - }), - [state, setState] - ); - - return {children}; -}; - -export const useAppState = () => { - const state = useContext(AppState); - return state; -}; diff --git a/packages/app/src/providers/app-state-provider/context.ts b/packages/app/src/providers/app-state-provider/context.ts new file mode 100644 index 0000000000..cf972024f5 --- /dev/null +++ b/packages/app/src/providers/app-state-provider/context.ts @@ -0,0 +1,49 @@ +import { createContext, useContext } from 'react'; +import { AccessTokenMessage } from '@pathfinder/data-services'; +import type { Workspace } from '@pathfinder/data-services'; +import type { + Workspace as StoreWorkspace, + Page as StorePage, +} from '@blocksuite/store'; +import type { EditorContainer } from '@blocksuite/editor'; + +export interface AppStateValue { + user: AccessTokenMessage | null; + workspacesMeta: Workspace[]; + + currentWorkspaceId: string; + currentWorkspace: StoreWorkspace | null; + + currentPage: StorePage | null; +} + +export interface AppStateContext extends AppStateValue { + setState: (state: AppStateValue) => void; + createEditor: (page: StorePage) => EditorContainer | null; + loadWorkspace?: ( + workspaceId: string + ) => Promise | null; + loadPage: (pageId: string) => Promise | null; + createPage: (pageId?: string) => Promise | null; +} + +export const AppState = createContext({ + user: null, + workspacesMeta: [], + + currentWorkspaceId: '', + currentWorkspace: null, + + currentPage: null, + + setState: () => {}, + createEditor: () => null, + loadWorkspace: undefined, + loadPage: () => null, + createPage: () => null, +}); + +export const useAppState = () => { + const state = useContext(AppState); + return state; +}; diff --git a/packages/app/src/providers/app-state-provider/dynamic-blocksuite.tsx b/packages/app/src/providers/app-state-provider/dynamic-blocksuite.tsx new file mode 100644 index 0000000000..9927e98279 --- /dev/null +++ b/packages/app/src/providers/app-state-provider/dynamic-blocksuite.tsx @@ -0,0 +1,76 @@ +import { useEffect } from 'react'; +import { + Workspace, + createWebsocketDocProvider, + IndexedDBDocProvider, +} from '@blocksuite/store'; +import type { Page } from '@blocksuite/store'; +import { BlockSchema, createEditor } from '@blocksuite/editor'; +import type { AppStateContext } from './context'; + +const getEditorParams = (workspaceId: string) => { + const providers = []; + const params = new URLSearchParams(location.search); + // const room = params.get('room') ?? 'AFFINE-pathfinder'; + if (params.get('syncMode') === 'websocket') { + const WebsocketDocProvider = createWebsocketDocProvider( + `ws://${window.location.host}/collaboration/` + ); + providers.push(WebsocketDocProvider); + } + + providers.push(IndexedDBDocProvider); + + return { + room: workspaceId, + providers, + }; +}; + +type LoadWorkspaceHandler = AppStateContext['loadWorkspace']; +type CreateEditorHandler = AppStateContext['createEditor']; + +interface Props { + setLoadWorkspaceHandler: (handler: LoadWorkspaceHandler) => void; + setCreateEditorHandler: (handler: CreateEditorHandler) => void; +} + +const DynamicBlocksuite = ({ + setLoadWorkspaceHandler, + setCreateEditorHandler, +}: Props) => { + useEffect(() => { + const openWorkspace: LoadWorkspaceHandler = (workspaceId: string) => + new Promise(resolve => { + const workspace = new Workspace({ + ...getEditorParams(workspaceId as string), + }).register(BlockSchema); + + const indexDBProvider = workspace.providers.find( + p => p instanceof IndexedDBDocProvider + ); + if (indexDBProvider) { + (indexDBProvider as IndexedDBDocProvider)?.on('synced', () => { + resolve(workspace); + }); + } else { + resolve(workspace); + } + }); + + setLoadWorkspaceHandler(openWorkspace); + }, [setLoadWorkspaceHandler]); + + useEffect(() => { + const createEditorHandler: CreateEditorHandler = (page: Page) => { + const editor = createEditor(page); + return editor; + }; + + setCreateEditorHandler(createEditorHandler); + }, [setCreateEditorHandler]); + + return <>; +}; + +export default DynamicBlocksuite; diff --git a/packages/app/src/providers/app-state-provider/hooks.ts b/packages/app/src/providers/app-state-provider/hooks.ts new file mode 100644 index 0000000000..b7f81ccf1f --- /dev/null +++ b/packages/app/src/providers/app-state-provider/hooks.ts @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useAppState } from './context'; + +export const useLoadWorkspace = () => { + const router = useRouter(); + const { loadWorkspace, currentWorkspace, currentWorkspaceId } = useAppState(); + + const workspaceId = router.query.workspaceId as string; + + useEffect(() => { + loadWorkspace?.(workspaceId); + }, [workspaceId]); + + return currentWorkspaceId === workspaceId ? currentWorkspace : null; +}; + +export const useLoadPage = () => { + const router = useRouter(); + const { loadPage, currentPage } = useAppState(); + const workspace = useLoadWorkspace(); + + const pageId = router.query.pageId as string; + + useEffect(() => { + loadPage(pageId); + }, [workspace, pageId]); + + return currentPage?.pageId === pageId ? currentPage : null; +}; diff --git a/packages/app/src/providers/app-state-provider/provider.tsx b/packages/app/src/providers/app-state-provider/provider.tsx new file mode 100644 index 0000000000..ba21a309fc --- /dev/null +++ b/packages/app/src/providers/app-state-provider/provider.tsx @@ -0,0 +1,125 @@ +import { useMemo, useState, useEffect, useCallback } from 'react'; +import type { ReactNode } from 'react'; +import dynamic from 'next/dynamic'; +import { + authorizationEvent, + AccessTokenMessage, + getWorkspaces, +} from '@pathfinder/data-services'; +import { AppState } from './context'; +import type { AppStateValue, AppStateContext } from './context'; + +const DynamicBlocksuite = dynamic(() => import('./dynamic-blocksuite'), { + ssr: false, +}); + +export const AppStateProvider = ({ children }: { children?: ReactNode }) => { + const [state, setState] = useState({ + user: null, + workspacesMeta: [], + + currentWorkspaceId: '', + currentWorkspace: null, + + currentPage: null, + }); + + const [loadWorkspaceHandler, _setLoadWorkspaceHandler] = + useState(); + const setLoadWorkspaceHandler = useCallback( + (handler: AppStateContext['loadWorkspace']) => { + _setLoadWorkspaceHandler(() => handler); + }, + [_setLoadWorkspaceHandler] + ); + + const [createEditorHandler, _setCreateEditorHandler] = + useState(); + const setCreateEditorHandler = useCallback( + (handler: AppStateContext['createEditor']) => { + _setCreateEditorHandler(() => handler); + }, + [_setCreateEditorHandler] + ); + + useEffect(() => { + const callback = async (user: AccessTokenMessage | null) => { + const workspaces = user ? await getWorkspaces() : []; + setState(state => ({ ...state, user: user, workspaces })); + }; + authorizationEvent.onChange(callback); + + return () => { + authorizationEvent.removeCallback(callback); + }; + }, []); + + const context = useMemo( + () => ({ + ...state, + setState, + createEditor: () => { + const { currentPage, currentWorkspace } = state; + if (!currentPage || !currentWorkspace) { + return null; + } + const editor = createEditorHandler?.(currentPage) || null; + + if (editor) { + const pageMeta = currentWorkspace?.meta.pageMetas.find( + p => p.id === currentPage.pageId + ); + if (pageMeta?.mode) { + editor.mode = pageMeta.mode as 'page' | 'edgeless' | undefined; + } + if (pageMeta?.trash) { + editor.readonly = true; + } + } + + return editor; + }, + loadWorkspace: async (workspaceId: string) => { + const workspace = (await loadWorkspaceHandler?.(workspaceId)) || null; + setState(state => ({ + ...state, + currentWorkspace: workspace, + currentWorkspaceId: workspaceId, + })); + return workspace; + }, + loadPage: async (pageId: string) => { + const { currentWorkspace, currentPage } = state; + if (pageId === currentPage?.pageId) { + return currentPage; + } + const page = currentWorkspace?.getPage(pageId) || null; + setState(state => ({ ...state, currentPage: page })); + return page; + }, + createPage: (pageId: string = Date.now().toString()) => + new Promise(resolve => { + const { currentWorkspace } = state; + if (!currentWorkspace) { + resolve(null); + return; + } + currentWorkspace.createPage(pageId); + currentWorkspace.signals.pageAdded.once(addedPageId => { + resolve(addedPageId); + }); + }), + }), + [state, setState, loadWorkspaceHandler, createEditorHandler] + ); + + return ( + + + {loadWorkspaceHandler ? children : null} + + ); +}; diff --git a/packages/app/src/providers/editor-provider/editor-provider.tsx b/packages/app/src/providers/editor-provider/editor-provider.tsx index cd66e0e74a..1f9d9a7d70 100644 --- a/packages/app/src/providers/editor-provider/editor-provider.tsx +++ b/packages/app/src/providers/editor-provider/editor-provider.tsx @@ -49,7 +49,7 @@ export const EditorProvider = ({ const setPage = useCallback( (page: Page) => { _setPage(page); - router.push(`/workspace/${workspaceId}/${page.id.replace('space:', '')}`); + // router.push(`/workspace/${workspaceId}/${page.id.replace('space:', '')}`); }, [_setPage, workspaceId] ); diff --git a/packages/app/src/providers/editor-provider/editor-reactor.tsx b/packages/app/src/providers/editor-provider/editor-reactor.tsx index 1deb6ca9e8..080de28c3c 100644 --- a/packages/app/src/providers/editor-provider/editor-reactor.tsx +++ b/packages/app/src/providers/editor-provider/editor-reactor.tsx @@ -53,8 +53,9 @@ const EditorReactor = ({ }) => { const shouldInitIntroduction = useRef(false); const { - query: { pageId: routerPageId, workspaceId }, + query: { pageId0: routerPageId }, } = useRouter(); + const workspaceId = 'sfas'; useEffect(() => { const workspace = new Workspace({