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:
DarkSky
2022-12-30 21:40:15 +08:00
committed by GitHub
parent cc790dcbc2
commit 6c2c7dcd48
296 changed files with 16139 additions and 2072 deletions

View 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);
};

View File

@@ -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;

View 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;
};

View File

@@ -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]);
};

View File

@@ -0,0 +1,2 @@
export * from './context';
export * from './interface';

View 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';
}

View 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>
);
};