import { appSidebarOpenAtom, appSidebarResizingAtom, } from '@affine/component/app-sidebar'; import { AppContainer, MainContainer, ToolContainer, WorkspaceFallback, } from '@affine/component/workspace'; import { DebugLogger } from '@affine/debug'; import { DEFAULT_HELLO_WORLD_PAGE_ID } from '@affine/env'; import { initPage } from '@affine/env/blocksuite'; import { setUpLanguage, useI18N } from '@affine/i18n'; import { createAffineGlobalChannel } from '@affine/workspace/affine/sync'; import { rootCurrentPageIdAtom, rootCurrentWorkspaceIdAtom, rootStore, rootWorkspacesMetadataAtom, } from '@affine/workspace/atom'; import type { BackgroundProvider } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { assertEquals, assertExists, nanoid } from '@blocksuite/store'; import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import Head from 'next/head'; import { useRouter } from 'next/router'; import type { FC, PropsWithChildren, ReactElement } from 'react'; import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react'; import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms'; import { publicWorkspaceAtom, publicWorkspaceIdAtom, } from '../atoms/public-workspace'; import { HelpIsland } from '../components/pure/help-island'; import { RootAppSidebar } from '../components/root-app-sidebar'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useRouterHelper } from '../hooks/use-router-helper'; import { useRouterTitle } from '../hooks/use-router-title'; import { useWorkspaces } from '../hooks/use-workspaces'; import { WorkspaceAdapters } from '../plugins'; import { ModalProvider } from '../providers/modal-provider'; import { pathGenerator, publicPathGenerator } from '../shared'; const QuickSearchModal = lazy(() => import('../components/pure/quick-search-modal').then(module => ({ default: module.QuickSearchModal, })) ); export const PublicQuickSearch: FC = () => { const publicWorkspace = useAtomValue(publicWorkspaceAtom); const router = useRouter(); const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom( openQuickSearchModalAtom ); return ( ); }; function DefaultProvider({ children }: PropsWithChildren) { return <>{children}; } export const QuickSearch: FC = () => { const [currentWorkspace] = useCurrentWorkspace(); const router = useRouter(); const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom( openQuickSearchModalAtom ); const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace; const isPublicWorkspace = router.pathname.split('/')[1] === 'public-workspace'; const publicWorkspaceId = useAtomValue(publicWorkspaceIdAtom); if (!blockSuiteWorkspace) { if (isPublicWorkspace && publicWorkspaceId) { return ; } return null; } return ( ); }; const logger = new DebugLogger('workspace-layout'); const affineGlobalChannel = createAffineGlobalChannel( WorkspaceAdapters[WorkspaceFlavour.AFFINE].CRUD ); export const AllWorkspaceContext = ({ children, }: PropsWithChildren): ReactElement => { const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom); const workspaces = useWorkspaces(); useEffect(() => { const providers = workspaces // ignore current workspace .filter(workspace => workspace.id !== currentWorkspaceId) .flatMap(workspace => workspace.providers.filter( (provider): provider is BackgroundProvider => 'background' in provider && provider.background ) ); providers.forEach(provider => { provider.connect(); }); return () => { providers.forEach(provider => { provider.disconnect(); }); }; }, [currentWorkspaceId, workspaces]); return <>{children}; }; export const CurrentWorkspaceContext = ({ children, }: PropsWithChildren): ReactElement => { const workspaceId = useAtomValue(rootCurrentWorkspaceIdAtom); const metadata = useAtomValue(rootWorkspacesMetadataAtom); const exist = metadata.find(m => m.id === workspaceId); const router = useRouter(); const push = router.push; useEffect(() => { const id = setTimeout(() => { if (!exist) { void push('/'); } }, 1000); return () => { clearTimeout(id); }; }, [push, exist]); if (!router.isReady) { return ; } if (!workspaceId) { return ; } if (!exist) { return ; } return <>{children}; }; export const WorkspaceLayout: FC = function WorkspacesSuspense({ children }) { const i18n = useI18N(); useEffect(() => { document.documentElement.lang = i18n.language; // todo(himself65): this is a hack, we should use a better way to set the language setUpLanguage(i18n); }, [i18n]); const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom); const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom); const meta = useMemo( () => jotaiWorkspaces.find(x => x.id === currentWorkspaceId), [currentWorkspaceId, jotaiWorkspaces] ); const set = useSetAtom(rootWorkspacesMetadataAtom); useEffect(() => { logger.info('mount'); const controller = new AbortController(); const lists = Object.values(WorkspaceAdapters) .sort((a, b) => a.loadPriority - b.loadPriority) .map(({ CRUD }) => CRUD.list); async function fetch() { const jotaiWorkspaces = rootStore.get(rootWorkspacesMetadataAtom); const items = []; for (const list of lists) { try { const item = await list(); if (jotaiWorkspaces.length) { item.sort((a, b) => { return ( jotaiWorkspaces.findIndex(x => x.id === a.id) - jotaiWorkspaces.findIndex(x => x.id === b.id) ); }); } items.push(...item.map(x => ({ id: x.id, flavour: x.flavour }))); } catch (e) { logger.error('list data error:', e); } } if (controller.signal.aborted) { return; } set([...items]); logger.info('mount first data:', items); } fetch(); return () => { controller.abort(); logger.info('unmount'); }; }, [set]); useEffect(() => { const flavour = jotaiWorkspaces.find( x => x.id === currentWorkspaceId )?.flavour; if (flavour === WorkspaceFlavour.AFFINE) { affineGlobalChannel.connect(); return () => { affineGlobalChannel.disconnect(); }; } }, [currentWorkspaceId, jotaiWorkspaces]); const Provider = (meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider; return ( <> {/* fixme(himself65): don't re-render whole modals */} }> {children} ); }; export const WorkspaceLayoutInner: FC = ({ children }) => { const [currentWorkspace] = useCurrentWorkspace(); const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom); const currentPageId = useAtomValue(rootCurrentPageIdAtom); const router = useRouter(); const { jumpToPage } = useRouterHelper(router); //#region init workspace if (currentWorkspace.blockSuiteWorkspace.isEmpty) { // this is a new workspace, so we should redirect to the new page const pageId = nanoid(); const page = currentWorkspace.blockSuiteWorkspace.createPage({ id: pageId, }); assertEquals(page.id, pageId); currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, { init: true, }); initPage(page); if (!router.query.pageId) { setCurrentPageId(pageId); void jumpToPage(currentWorkspace.id, pageId); } } // fixme: pinboard has been removed, // the related code should be removed in the future. // no matter the workspace is empty, ensure the root pinboard exists // ensureRootPinboard(currentWorkspace.blockSuiteWorkspace); //#endregion useEffect(() => { const backgroundProviders = currentWorkspace.providers.filter( (provider): provider is BackgroundProvider => 'background' in provider ); backgroundProviders.forEach(provider => { provider.connect(); }); return () => { backgroundProviders.forEach(provider => { provider.disconnect(); }); }; }, [currentWorkspace]); useEffect(() => { if (!currentWorkspace) { return; } const page = currentWorkspace.blockSuiteWorkspace.getPage( DEFAULT_HELLO_WORLD_PAGE_ID ); if (page && page.meta.jumpOnce) { currentWorkspace.blockSuiteWorkspace.meta.setPageMeta( DEFAULT_HELLO_WORLD_PAGE_ID, { jumpOnce: false, } ); setCurrentPageId(currentPageId); void jumpToPage(currentWorkspace.id, page.id); } }, [ currentPageId, currentWorkspace, jumpToPage, router.query.pageId, setCurrentPageId, ]); const { openPage } = useRouterHelper(router); const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom); const helper = useBlockSuiteWorkspaceHelper( currentWorkspace.blockSuiteWorkspace ); const isPublicWorkspace = router.pathname.split('/')[1] === 'public-workspace'; const title = useRouterTitle(router); const handleCreatePage = useCallback(() => { return helper.createPage(nanoid()); }, [helper]); const handleOpenWorkspaceListModal = useCallback(() => { setOpenWorkspacesModal(true); }, [setOpenWorkspacesModal]); const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom); const handleOpenQuickSearchModal = useCallback(() => { setOpenQuickSearchModalAtom(true); }, [setOpenQuickSearchModalAtom]); const resizing = useAtomValue(appSidebarResizingAtom); const sidebarOpen = useAtomValue(appSidebarOpenAtom); return ( <> {title} { assertExists(currentWorkspace); return openPage(currentWorkspace.id, pageId); }, [currentWorkspace, openPage] )} createPage={handleCreatePage} currentPath={router.asPath.split('?')[0]} paths={isPublicWorkspace ? publicPathGenerator : pathGenerator} /> }>{children} {/* fixme(himself65): remove this */}
{/* Slot for block hub */}
{!isPublicWorkspace && ( )}
); };