refactor!: next generation AFFiNE code structure (#1176)

This commit is contained in:
Himself65
2023-03-01 01:40:01 -06:00
committed by GitHub
parent 2dcccc772c
commit e0481d29ad
270 changed files with 8308 additions and 6829 deletions

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { SWRConfig, SWRConfiguration } from 'swr';
import { fetcher } from '../plugins/affine/fetcher';
const config: SWRConfiguration = {
suspense: true,
fetcher,
};
export const AffineSWRConfigProvider = React.memo<React.PropsWithChildren>(
function AffineSWRConfigProvider({ children }) {
return <SWRConfig value={config}>{children}</SWRConfig>;
}
);

View File

@@ -1,101 +0,0 @@
import { Confirm, ConfirmProps } from '@affine/component';
import type { PropsWithChildren } from 'react';
import { createContext, useContext, useMemo } from 'react';
import { createStore, useStore } from 'zustand';
import { combine, subscribeWithSelector } from 'zustand/middleware';
import { UseBoundStore } from 'zustand/react';
type ConfirmActions = {
confirm: (props: ConfirmProps) => Promise<boolean>;
};
type ConfirmState = {
record: Record<string, JSX.Element>;
};
const create = () =>
createStore(
subscribeWithSelector(
combine<ConfirmState, ConfirmActions>(
{
record: {},
},
(set, get) => ({
confirm: ({ onCancel, onConfirm, ...props }: ConfirmProps) => {
return new Promise(resolve => {
const confirmRecord = { ...get().record };
const confirmId = String(Date.now());
const closeHandler = () => {
delete confirmRecord[confirmId];
set({ record: { ...confirmRecord } });
};
set(({ record }) => {
return {
record: {
...record,
[confirmId]: (
<Confirm
{...props}
onCancel={() => {
closeHandler();
onCancel?.();
resolve(false);
}}
onConfirm={() => {
closeHandler();
onConfirm?.();
resolve(true);
}}
/>
),
},
};
});
});
},
})
)
)
);
type Store = ReturnType<typeof create>;
export const ConfirmContext = createContext<Store | null>(null);
export const useConfirmApi = () => {
const api = useContext(ConfirmContext);
if (!api) {
throw new Error('cannot find confirm context');
}
return api;
};
export const useConfirm: UseBoundStore<Store> = ((
selector: Parameters<UseBoundStore<Store>>[0],
equals: Parameters<UseBoundStore<Store>>[1]
) => {
const api = useConfirmApi();
return useStore(api, selector, equals);
}) as any;
function Records() {
const conform = useConfirm(store => store.record);
return (
<>
{Object.entries(conform).map(([confirmId, confirmNode]) => {
return <div key={confirmId}>{confirmNode}</div>;
})}
</>
);
}
export const ConfirmProvider = ({ children }: PropsWithChildren) => {
return (
<ConfirmContext.Provider value={useMemo(() => create(), [])}>
{children}
<Records />
</ConfirmContext.Provider>
);
};
export default ConfirmProvider;

View File

@@ -0,0 +1,118 @@
import { useAtom } from 'jotai';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import {
openCreateWorkspaceModalAtom,
openQuickSearchModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { CreateWorkspaceModal } from '../components/pure/create-workspace-modal';
import QuickSearchModal from '../components/pure/quick-search-modal';
import { WorkspaceListModal } from '../components/pure/workspace-list-modal';
import { useCurrentUser } from '../hooks/current/use-current-user';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces';
import { apis } from '../shared/apis';
export function Modals() {
const [openWorkspacesModal, setOpenWorkspacesModal] = useAtom(
openWorkspacesModalAtom
);
const [openCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
openCreateWorkspaceModalAtom
);
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
openQuickSearchModalAtom
);
const router = useRouter();
const user = useCurrentUser();
const workspaces = useWorkspaces();
const [currentWorkspace, setCurrentWorkspace] = useCurrentWorkspace();
const { createRemLocalWorkspace } = useWorkspacesHelper();
const disableShortCut = router.pathname.startsWith('/404');
return (
<>
<WorkspaceListModal
user={user}
workspaces={workspaces}
currentWorkspaceId={currentWorkspace?.id ?? null}
open={openWorkspacesModal}
onClose={useCallback(() => {
setOpenWorkspacesModal(false);
}, [setOpenWorkspacesModal])}
onClickWorkspace={useCallback(
workspace => {
setCurrentWorkspace(workspace.id);
router.push({
pathname: `/workspace/[workspaceId]/all`,
query: {
workspaceId: workspace.id,
},
});
setOpenWorkspacesModal(false);
},
[router, setCurrentWorkspace, setOpenWorkspacesModal]
)}
onClickLogin={useCallback(() => {
apis.signInWithGoogle().then(() => {
router.reload();
});
}, [router])}
onClickLogout={useCallback(() => {
apis.auth.clear();
router.reload();
}, [router])}
onCreateWorkspace={useCallback(() => {
setOpenCreateWorkspaceModal(true);
}, [setOpenCreateWorkspaceModal])}
/>
<CreateWorkspaceModal
open={openCreateWorkspaceModal}
onClose={useCallback(() => {
setOpenCreateWorkspaceModal(false);
}, [setOpenCreateWorkspaceModal])}
onCreate={useCallback(
name => {
const id = createRemLocalWorkspace(name);
setOpenCreateWorkspaceModal(false);
setOpenWorkspacesModal(false);
router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: id,
},
});
},
[
createRemLocalWorkspace,
router,
setOpenCreateWorkspaceModal,
setOpenWorkspacesModal,
]
)}
/>
{currentWorkspace?.blockSuiteWorkspace && (
<QuickSearchModal
enableShortCut={!disableShortCut}
blockSuiteWorkspace={currentWorkspace?.blockSuiteWorkspace}
open={openQuickSearchModal}
setOpen={setOpenQuickSearchModalAtom}
router={router}
/>
)}
</>
);
}
export const ModalProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
return (
<>
<Modals />
{children}
</>
);
};

View File

@@ -1,4 +1,5 @@
import {
AffineTheme,
Theme,
ThemeMode,
ThemeProviderProps,
@@ -10,16 +11,27 @@ import {
globalThemeVariables,
ThemeProvider as ComponentThemeProvider,
} from '@affine/component';
import { localStorageThemeHelper, SystemThemeHelper } from '@affine/component';
import { css, Global } from '@emotion/react';
import { GlobalStyles } from '@mui/material';
import {
createTheme as MuiCreateTheme,
ThemeProvider as MuiThemeProvider,
} from '@mui/material/styles';
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import type { PropsWithChildren } from 'react';
import { createContext, useContext, useEffect, useState } from 'react';
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
} from 'react';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useCurrentPageId } from '../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { usePageMeta } from '../hooks/use-page-meta';
import { useSystemTheme } from '../hooks/use-system-theme';
export const ThemeContext = createContext<ThemeProviderValue>({
mode: 'light',
@@ -31,76 +43,65 @@ export const ThemeContext = createContext<ThemeProviderValue>({
export const useTheme = () => useContext(ThemeContext);
const muiTheme = MuiCreateTheme();
const ThemeInjector = React.memo<{
theme: Theme;
themeStyle: AffineTheme;
}>(function ThemeInjector({ theme, themeStyle }) {
return (
<GlobalStyles
styles={{
':root': globalThemeVariables(theme, themeStyle) as any,
}}
/>
);
});
const themeAtom = atomWithStorage<ThemeMode>('affine-theme', 'auto');
export const ThemeProvider = ({
defaultTheme = 'light',
children,
}: PropsWithChildren<ThemeProviderProps>) => {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const [mode, setMode] = useState<ThemeMode>('auto');
const { mode: editorMode = 'page' } = useCurrentPageMeta() || {};
const themeStyle =
theme === 'light' ? getLightTheme(editorMode) : getDarkTheme(editorMode);
const changeMode = (themeMode: ThemeMode) => {
themeMode !== mode && setMode(themeMode);
// Remember the theme mode which user selected for next time
localStorageThemeHelper.set(themeMode);
};
const [theme, setTheme] = useAtom(themeAtom);
const systemTheme = useSystemTheme();
// fixme: use mode detect
const [currentWorkspace] = useCurrentWorkspace();
const [currentPage] = useCurrentPageId();
const pageMeta = usePageMeta(currentWorkspace?.blockSuiteWorkspace ?? null);
const editorMode =
pageMeta.find(page => page.id === currentPage)?.mode ?? 'page';
const themeStyle = useMemo(
() =>
theme === 'light' ? getLightTheme(editorMode) : getDarkTheme(editorMode),
[editorMode, theme]
);
const changeMode = useCallback(
(themeMode: Theme) => {
setTheme(themeMode);
},
[setTheme]
);
// ===================== A temporary solution, just use system theme and not remember the user selected ====================
useEffect(() => {
const systemThemeHelper = new SystemThemeHelper();
const systemTheme = systemThemeHelper.get();
setMode(systemTheme);
systemThemeHelper.onChange(() => {
setMode(systemThemeHelper.get());
});
}, []);
const onceRef = useRef(false);
useEffect(() => {
setTheme(mode === 'auto' ? theme : mode);
}, [mode, setTheme, theme]);
// ===================== ====================
if (onceRef.current) {
return;
}
if (theme !== 'auto') {
setTheme(systemTheme);
}
onceRef.current = true;
}, [setTheme, systemTheme, theme]);
// useEffect(() => {
// setMode(localStorageThemeHelper.get() || 'auto');
// }, []);
//
// useEffect(() => {
// const systemThemeHelper = new SystemThemeHelper();
// const selectedThemeMode = localStorageThemeHelper.get();
//
// const themeMode = selectedThemeMode || mode;
// if (themeMode === 'auto') {
// setTheme(systemThemeHelper.get());
// } else {
// setTheme(themeMode);
// }
//
// // When system theme changed, change the theme mode
// systemThemeHelper.onChange(() => {
// // TODO: There may be should be provided a way to let user choose whether to
// if (mode === 'auto') {
// setTheme(systemThemeHelper.get());
// }
// });
//
// return () => {
// systemThemeHelper.dispose();
// };
// }, [mode]);
const realTheme: ThemeMode = theme === 'auto' ? systemTheme : theme;
return (
// Use MuiThemeProvider is just because some Transitions in Mui components need it
<MuiThemeProvider theme={muiTheme}>
<ThemeContext.Provider value={{ mode, changeMode, theme: themeStyle }}>
<Global
styles={css`
:root {
${globalThemeVariables(mode, themeStyle) as any}
}
`}
/>
<ThemeContext.Provider
value={{ mode: realTheme, changeMode, theme: themeStyle }}
>
<ThemeInjector theme={realTheme} themeStyle={themeStyle} />
<ComponentThemeProvider theme={themeStyle}>
{children}
</ComponentThemeProvider>
@@ -108,5 +109,3 @@ export const ThemeProvider = ({
</MuiThemeProvider>
);
};
export default ThemeProvider;

View File

@@ -1,51 +0,0 @@
import type { Disposable } from '@blocksuite/global/utils';
import type { PropsWithChildren } from 'react';
import { createContext, useContext, useEffect, useState } from 'react';
import { useGlobalState } from '@/store/app';
import { AppStateContext } from './interface';
type AppStateContextProps = PropsWithChildren<Record<string, unknown>>;
export const AppState = createContext<AppStateContext>({} as AppStateContext);
export const useAppState = () => useContext(AppState);
export const AppStateProvider = ({
children,
}: PropsWithChildren<AppStateContextProps>) => {
const currentDataCenterWorkspace = useGlobalState(
store => store.currentDataCenterWorkspace
);
const [blobState, setBlobState] = useState(false);
useEffect(() => {
let syncChangeDisposable: Disposable | undefined;
const currentWorkspace = currentDataCenterWorkspace;
if (!currentWorkspace) {
return;
}
const getBlobStorage = async () => {
const blobStorage = await currentWorkspace?.blocksuiteWorkspace?.blobs;
syncChangeDisposable = blobStorage?.signals.onBlobSyncStateChange.on(
() => {
setBlobState(blobStorage?.uploading);
}
);
};
getBlobStorage();
return () => {
syncChangeDisposable?.dispose();
};
}, [currentDataCenterWorkspace]);
return (
<AppState.Provider
value={{
blobDataSynced: blobState,
}}
>
{children}
</AppState.Provider>
);
};

View File

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

View File

@@ -1,28 +0,0 @@
import type { EditorContainer } from '@blocksuite/editor';
import type {
Page as StorePage,
PageMeta as StorePageMeta,
} from '@blocksuite/store';
export interface PageMeta extends StorePageMeta {
favorite: boolean;
trash: boolean;
trashDate: number;
updatedDate: number;
mode: 'edgeless' | 'page';
}
export type AppStateValue = {
blobDataSynced: boolean;
};
/**
* @deprecated
*/
export type AppStateFunction = {
// todo: remove this in the future
};
export type AppStateContext = AppStateValue & AppStateFunction;
export type CreateEditorHandler = (page: StorePage) => EditorContainer | null;

View File

@@ -1,9 +0,0 @@
import { DataCenter } from '@affine/datacenter';
const DEFAULT_WORKSPACE_NAME = 'Demo Workspace';
export const createDefaultWorkspace = async (dataCenter: DataCenter) => {
return dataCenter.createWorkspace({
name: DEFAULT_WORKSPACE_NAME,
});
};