mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
milestone: publish alpha version (#637)
- document folder - full-text search - blob storage - basic edgeless support Co-authored-by: tzhangchi <terry.zhangchi@outlook.com> Co-authored-by: QiShaoXuan <qishaoxuan777@gmail.com> Co-authored-by: DiamondThree <diamond.shx@gmail.com> Co-authored-by: MingLiang Wang <mingliangwang0o0@gmail.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Yifeng Wang <doodlewind@toeverything.info> Co-authored-by: Himself65 <himself65@outlook.com> Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com>
This commit is contained in:
67
packages/app/src/providers/app-state-provider/context.ts
Normal file
67
packages/app/src/providers/app-state-provider/context.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { createContext, MutableRefObject, useContext } from 'react';
|
||||
import type { Workspace } from '@affine/data-services';
|
||||
import { AccessTokenMessage } from '@affine/data-services';
|
||||
import type {
|
||||
Page as StorePage,
|
||||
Workspace as StoreWorkspace,
|
||||
} from '@blocksuite/store';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
export type LoadWorkspaceHandler = (
|
||||
workspaceId: string,
|
||||
websocket?: boolean,
|
||||
user?: AccessTokenMessage | null
|
||||
) => Promise<StoreWorkspace | null> | null;
|
||||
export type CreateEditorHandler = (page: StorePage) => EditorContainer | null;
|
||||
|
||||
export interface AppStateValue {
|
||||
user: AccessTokenMessage | null;
|
||||
workspacesMeta: Workspace[];
|
||||
|
||||
currentWorkspaceId: string;
|
||||
currentWorkspace: StoreWorkspace | null;
|
||||
|
||||
currentPage: StorePage | null;
|
||||
|
||||
workspaces: Record<string, StoreWorkspace | null>;
|
||||
|
||||
editor: EditorContainer | null;
|
||||
synced: boolean;
|
||||
refreshWorkspacesMeta: () => void;
|
||||
}
|
||||
|
||||
export interface AppStateContext extends AppStateValue {
|
||||
setState: (state: AppStateValue) => void;
|
||||
createEditor?: MutableRefObject<
|
||||
((page: StorePage) => EditorContainer | null) | undefined
|
||||
>;
|
||||
setEditor?: MutableRefObject<((page: EditorContainer) => void) | undefined>;
|
||||
loadWorkspace: (workspaceId: string) => Promise<StoreWorkspace | null>;
|
||||
loadPage: (pageId: string) => Promise<StorePage | null>;
|
||||
}
|
||||
|
||||
export const AppState = createContext<AppStateContext>({
|
||||
user: null,
|
||||
workspacesMeta: [],
|
||||
|
||||
currentWorkspaceId: '',
|
||||
currentWorkspace: null,
|
||||
|
||||
currentPage: null,
|
||||
|
||||
editor: null,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setState: () => {},
|
||||
createEditor: undefined,
|
||||
setEditor: undefined,
|
||||
loadWorkspace: () => Promise.resolve(null),
|
||||
loadPage: () => Promise.resolve(null),
|
||||
synced: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
refreshWorkspacesMeta: () => {},
|
||||
workspaces: {},
|
||||
});
|
||||
|
||||
export const useAppState = () => {
|
||||
return useContext(AppState);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import {
|
||||
IndexedDBDocProvider,
|
||||
Workspace as StoreWorkspace,
|
||||
} from '@blocksuite/store';
|
||||
import '@blocksuite/blocks';
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
import { BlockSchema } from '@blocksuite/blocks/models';
|
||||
import type { LoadWorkspaceHandler, CreateEditorHandler } from './context';
|
||||
|
||||
interface Props {
|
||||
setLoadWorkspaceHandler: (handler: LoadWorkspaceHandler) => void;
|
||||
setCreateEditorHandler: (handler: CreateEditorHandler) => void;
|
||||
}
|
||||
|
||||
const DynamicBlocksuite = ({
|
||||
setLoadWorkspaceHandler,
|
||||
setCreateEditorHandler,
|
||||
}: Props) => {
|
||||
useEffect(() => {
|
||||
const openWorkspace: LoadWorkspaceHandler = (
|
||||
workspaceId: string,
|
||||
websocket = false,
|
||||
user
|
||||
) =>
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
new Promise(async resolve => {
|
||||
const workspace = new StoreWorkspace({
|
||||
room: workspaceId,
|
||||
providers: [IndexedDBDocProvider],
|
||||
}).register(BlockSchema);
|
||||
console.log('websocket', websocket);
|
||||
console.log('user', user);
|
||||
|
||||
// if (websocket && token.refresh) {
|
||||
// // FIXME: if add websocket provider, the first page will be blank
|
||||
// const ws = new WebsocketProvider(
|
||||
// `ws${window.location.protocol === 'https:' ? 's' : ''}://${
|
||||
// window.location.host
|
||||
// }/api/sync/`,
|
||||
// workspaceId,
|
||||
// workspace.doc,
|
||||
// {
|
||||
// params: {
|
||||
// token: token.refresh,
|
||||
// },
|
||||
// awareness: workspace.meta.awareness.awareness,
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// ws.shouldConnect = false;
|
||||
//
|
||||
// // FIXME: there needs some method to destroy websocket.
|
||||
// // Or we need a manager to manage websocket.
|
||||
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// // @ts-expect-error
|
||||
// workspace.__ws__ = ws;
|
||||
// }
|
||||
|
||||
const indexDBProvider = workspace.providers.find(
|
||||
p => p instanceof IndexedDBDocProvider
|
||||
);
|
||||
// if (user) {
|
||||
// const updates = await downloadWorkspace({ workspaceId });
|
||||
// updates &&
|
||||
// StoreWorkspace.Y.applyUpdate(
|
||||
// workspace.doc,
|
||||
// new Uint8Array(updates)
|
||||
// );
|
||||
// // if after update, the space:meta is empty, then we need to get map with doc
|
||||
// workspace.doc.getMap('space:meta');
|
||||
// }
|
||||
if (indexDBProvider) {
|
||||
(indexDBProvider as IndexedDBDocProvider).whenSynced.then(() => {
|
||||
resolve(workspace);
|
||||
});
|
||||
} else {
|
||||
resolve(workspace);
|
||||
}
|
||||
});
|
||||
|
||||
setLoadWorkspaceHandler(openWorkspace);
|
||||
}, [setLoadWorkspaceHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
const createEditorHandler: CreateEditorHandler = (page: Page) => {
|
||||
const editor = new EditorContainer();
|
||||
editor.page = page;
|
||||
return editor;
|
||||
};
|
||||
|
||||
setCreateEditorHandler(createEditorHandler);
|
||||
}, [setCreateEditorHandler]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default DynamicBlocksuite;
|
||||
51
packages/app/src/providers/app-state-provider/hooks.ts
Normal file
51
packages/app/src/providers/app-state-provider/hooks.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from './context';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
export const useLoadWorkspace = () => {
|
||||
const router = useRouter();
|
||||
const { loadWorkspace, currentWorkspace, currentWorkspaceId } = useAppState();
|
||||
|
||||
const workspaceId = router.query.workspaceId as string;
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkspace?.(workspaceId);
|
||||
}, [workspaceId, loadWorkspace]);
|
||||
|
||||
return currentWorkspaceId === workspaceId ? currentWorkspace : null;
|
||||
};
|
||||
|
||||
export const useLoadPage = () => {
|
||||
const router = useRouter();
|
||||
const { loadPage, currentPage, currentWorkspaceId } = useAppState();
|
||||
const { createPage } = usePageHelper();
|
||||
const workspace = useLoadWorkspace();
|
||||
|
||||
const pageId = router.query.pageId as string;
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
const page = pageId ? workspace?.getPage(pageId) : null;
|
||||
if (page) {
|
||||
loadPage?.(pageId);
|
||||
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, pageId, loadPage, createPage, router, currentWorkspaceId]);
|
||||
|
||||
return currentPage?.id === pageId ? currentPage : null;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
AccessTokenMessage,
|
||||
getWorkspaces,
|
||||
token,
|
||||
} from '@affine/data-services';
|
||||
import { LoadWorkspaceHandler } from '../context';
|
||||
|
||||
export const useSyncData = ({
|
||||
loadWorkspaceHandler,
|
||||
}: {
|
||||
loadWorkspaceHandler: LoadWorkspaceHandler;
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (!loadWorkspaceHandler) {
|
||||
return;
|
||||
}
|
||||
const start = async () => {
|
||||
const isLogin = await token.refreshToken().catch(() => false);
|
||||
return isLogin;
|
||||
};
|
||||
start();
|
||||
|
||||
const callback = async (user: AccessTokenMessage | null) => {
|
||||
const workspacesMeta = user
|
||||
? await getWorkspaces().catch(() => {
|
||||
return [];
|
||||
})
|
||||
: [];
|
||||
// setState(state => ({
|
||||
// ...state,
|
||||
// user: user,
|
||||
// workspacesMeta,
|
||||
// synced: true,
|
||||
// }));
|
||||
return workspacesMeta;
|
||||
};
|
||||
|
||||
token.onChange(callback);
|
||||
token.refreshToken().catch(err => {
|
||||
// FIXME: should resolve invalid refresh token
|
||||
console.log(err);
|
||||
});
|
||||
return () => {
|
||||
token.offChange(callback);
|
||||
};
|
||||
}, [loadWorkspaceHandler]);
|
||||
};
|
||||
2
packages/app/src/providers/app-state-provider/index.ts
Normal file
2
packages/app/src/providers/app-state-provider/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './context';
|
||||
export * from './interface';
|
||||
17
packages/app/src/providers/app-state-provider/interface.ts
Normal file
17
packages/app/src/providers/app-state-provider/interface.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PageMeta as OriginalPageMeta } from '@blocksuite/store/dist/workspace/workspace';
|
||||
|
||||
// export type PageMeta = {
|
||||
// favorite: boolean;
|
||||
// trash: boolean;
|
||||
// trashDate: number | void;
|
||||
// updatedDate: number | void;
|
||||
// mode: EditorContainer['mode'];
|
||||
// } & OriginalPageMeta;
|
||||
|
||||
export interface PageMeta extends OriginalPageMeta {
|
||||
favorite: boolean;
|
||||
trash: boolean;
|
||||
trashDate: number;
|
||||
updatedDate: number;
|
||||
mode: 'edgeless' | 'page';
|
||||
}
|
||||
181
packages/app/src/providers/app-state-provider/provider.tsx
Normal file
181
packages/app/src/providers/app-state-provider/provider.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { getWorkspaces } from '@affine/data-services';
|
||||
import { AppState, AppStateContext } from './context';
|
||||
import type {
|
||||
AppStateValue,
|
||||
CreateEditorHandler,
|
||||
LoadWorkspaceHandler,
|
||||
} from './context';
|
||||
import { Page, Workspace as StoreWorkspace } from '@blocksuite/store';
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
const DynamicBlocksuite = dynamic(() => import('./dynamic-blocksuite'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
|
||||
const refreshWorkspacesMeta = async () => {
|
||||
const workspacesMeta = await getWorkspaces().catch(() => {
|
||||
return [];
|
||||
});
|
||||
setState(state => ({ ...state, workspacesMeta }));
|
||||
};
|
||||
|
||||
const [state, setState] = useState<AppStateValue>({
|
||||
user: null,
|
||||
workspacesMeta: [],
|
||||
currentWorkspaceId: '',
|
||||
currentWorkspace: null,
|
||||
currentPage: null,
|
||||
editor: null,
|
||||
// Synced is used to ensure that the provider has synced with the server,
|
||||
// So after Synced set to true, the other state is sure to be set.
|
||||
synced: false,
|
||||
refreshWorkspacesMeta,
|
||||
workspaces: {},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const workspacesList = await Promise.all(
|
||||
state.workspacesMeta.map(async ({ id }) => {
|
||||
const workspace =
|
||||
(await loadWorkspaceHandler?.(id, false, state.user)) || null;
|
||||
return { id, workspace };
|
||||
})
|
||||
);
|
||||
const workspaces: Record<string, StoreWorkspace | null> = {};
|
||||
workspacesList.forEach(({ id, workspace }) => {
|
||||
workspaces[id] = workspace;
|
||||
});
|
||||
setState(state => ({
|
||||
...state,
|
||||
workspaces,
|
||||
}));
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state.workspacesMeta]);
|
||||
|
||||
const [loadWorkspaceHandler, _setLoadWorkspaceHandler] =
|
||||
useState<LoadWorkspaceHandler>();
|
||||
const setLoadWorkspaceHandler = useCallback(
|
||||
(handler: LoadWorkspaceHandler) => {
|
||||
_setLoadWorkspaceHandler(() => handler);
|
||||
},
|
||||
[_setLoadWorkspaceHandler]
|
||||
);
|
||||
|
||||
const [createEditorHandler, _setCreateEditorHandler] =
|
||||
useState<CreateEditorHandler>();
|
||||
|
||||
const setCreateEditorHandler = useCallback(
|
||||
(handler: CreateEditorHandler) => {
|
||||
_setCreateEditorHandler(() => handler);
|
||||
},
|
||||
[_setCreateEditorHandler]
|
||||
);
|
||||
|
||||
const loadWorkspace = useRef<AppStateContext['loadWorkspace']>(() =>
|
||||
Promise.resolve(null)
|
||||
);
|
||||
loadWorkspace.current = async (workspaceId: string) => {
|
||||
if (state.currentWorkspaceId === workspaceId) {
|
||||
return state.currentWorkspace;
|
||||
}
|
||||
const workspace =
|
||||
(await loadWorkspaceHandler?.(workspaceId, true, state.user)) || null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
window.workspace = workspace;
|
||||
// FIXME: there needs some method to destroy websocket.
|
||||
// Or we need a manager to manage websocket.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
state.currentWorkspace?.__ws__?.destroy();
|
||||
|
||||
setState(state => ({
|
||||
...state,
|
||||
currentWorkspace: workspace,
|
||||
currentWorkspaceId: workspaceId,
|
||||
}));
|
||||
return workspace;
|
||||
};
|
||||
const loadPage = useRef<AppStateContext['loadPage']>(() =>
|
||||
Promise.resolve(null)
|
||||
);
|
||||
loadPage.current = async (pageId: string) => {
|
||||
const { currentWorkspace, currentPage } = state;
|
||||
if (pageId === currentPage?.id) {
|
||||
return currentPage;
|
||||
}
|
||||
const page = (pageId ? currentWorkspace?.getPage(pageId) : null) || null;
|
||||
setState(state => ({ ...state, currentPage: page }));
|
||||
return page;
|
||||
};
|
||||
|
||||
const createEditor = useRef<
|
||||
((page: Page) => EditorContainer | null) | undefined
|
||||
>();
|
||||
createEditor.current = () => {
|
||||
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.id
|
||||
);
|
||||
if (pageMeta?.mode) {
|
||||
editor.mode = pageMeta.mode as 'page' | 'edgeless' | undefined;
|
||||
}
|
||||
if (pageMeta?.trash) {
|
||||
editor.readonly = true;
|
||||
}
|
||||
}
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
const setEditor = useRef<(editor: AppStateValue['editor']) => void>();
|
||||
|
||||
setEditor.current = (editor: AppStateValue['editor']) => {
|
||||
setState(state => ({ ...state, editor }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadWorkspaceHandler) {
|
||||
return;
|
||||
}
|
||||
setState(state => ({
|
||||
...state,
|
||||
workspacesMeta: [],
|
||||
synced: true,
|
||||
}));
|
||||
}, [loadWorkspaceHandler]);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
setState,
|
||||
createEditor,
|
||||
setEditor,
|
||||
loadWorkspace: loadWorkspace.current,
|
||||
loadPage: loadPage.current,
|
||||
}),
|
||||
[state, setState, loadPage, loadWorkspace]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppState.Provider value={context}>
|
||||
<DynamicBlocksuite
|
||||
setLoadWorkspaceHandler={setLoadWorkspaceHandler}
|
||||
setCreateEditorHandler={setCreateEditorHandler}
|
||||
/>
|
||||
{children}
|
||||
</AppState.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user