mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-23 05:33:08 +03:00
feat: init mobile entry (#7905)
This commit is contained in:
parent
3db95bafa2
commit
5acf1b5309
@ -73,7 +73,7 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
||||
);
|
||||
};
|
||||
|
||||
const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
|
||||
export const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
|
||||
const t = useI18n();
|
||||
const pushGlobalLoadingEvent = useSetAtom(pushGlobalLoadingEventAtom);
|
||||
const resolveGlobalLoadingEvent = useSetAtom(resolveGlobalLoadingEventAtom);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FrameworkScope, useLiveData } from '@toeverything/infra';
|
||||
import { lazy as reactLazy, useLayoutEffect, useMemo } from 'react';
|
||||
import { useLayoutEffect, useMemo } from 'react';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import {
|
||||
createMemoryRouter,
|
||||
RouterProvider,
|
||||
@ -7,30 +8,16 @@ import {
|
||||
UNSAFE_RouteContext,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { viewRoutes } from '../../../router';
|
||||
import type { View } from '../entities/view';
|
||||
import { RouteContainer } from './route-container';
|
||||
|
||||
const warpedRoutes = viewRoutes.map(({ path, lazy }) => {
|
||||
const Component = reactLazy(() =>
|
||||
lazy().then(m => ({
|
||||
default: m.Component as React.ComponentType,
|
||||
}))
|
||||
);
|
||||
const route = {
|
||||
Component,
|
||||
};
|
||||
|
||||
return {
|
||||
path,
|
||||
Component: () => {
|
||||
return <RouteContainer route={route} />;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const ViewRoot = ({ view }: { view: View }) => {
|
||||
const viewRouter = useMemo(() => createMemoryRouter(warpedRoutes), []);
|
||||
export const ViewRoot = ({
|
||||
view,
|
||||
routes,
|
||||
}: {
|
||||
view: View;
|
||||
routes: RouteObject[];
|
||||
}) => {
|
||||
const viewRouter = useMemo(() => createMemoryRouter(routes), [routes]);
|
||||
|
||||
const location = useLiveData(view.location$);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ResizePanel } from '@affine/component/resize-panel';
|
||||
import { rightSidebarWidthAtom } from '@affine/core/atoms';
|
||||
import { viewRoutes } from '@affine/core/router';
|
||||
import {
|
||||
appSettingAtom,
|
||||
FrameworkScope,
|
||||
@ -7,13 +8,21 @@ import {
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
lazy as reactLazy,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import type { View } from '../entities/view';
|
||||
import { WorkbenchService } from '../services/workbench';
|
||||
import { useBindWorkbenchToBrowserRouter } from './browser-adapter';
|
||||
import { useBindWorkbenchToDesktopRouter } from './desktop-adapter';
|
||||
import { RouteContainer } from './route-container';
|
||||
import { SidebarContainer } from './sidebar/sidebar-container';
|
||||
import { SplitView } from './split-view/split-view';
|
||||
import { ViewIslandRegistryProvider } from './view-islands';
|
||||
@ -24,6 +33,24 @@ const useAdapter = environment.isDesktop
|
||||
? useBindWorkbenchToDesktopRouter
|
||||
: useBindWorkbenchToBrowserRouter;
|
||||
|
||||
const warpedRoutes = viewRoutes.map(({ path, lazy }) => {
|
||||
const Component = reactLazy(() =>
|
||||
lazy().then(m => ({
|
||||
default: m.Component as React.ComponentType,
|
||||
}))
|
||||
);
|
||||
const route = {
|
||||
Component,
|
||||
};
|
||||
|
||||
return {
|
||||
path,
|
||||
Component: () => {
|
||||
return <RouteContainer route={route} />;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const WorkbenchRoot = memo(() => {
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
@ -93,7 +120,7 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
|
||||
|
||||
return (
|
||||
<div className={styles.workbenchViewContainer} ref={containerRef}>
|
||||
<ViewRoot key={view.id} view={view} />
|
||||
<ViewRoot routes={warpedRoutes} key={view.id} view={view} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useBindWorkbenchToBrowserRouter } from '@affine/core/modules/workbench/view/browser-adapter';
|
||||
import { ViewRoot } from '@affine/core/modules/workbench/view/view-root';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
import { type RouteObject, useLocation } from 'react-router-dom';
|
||||
|
||||
export const MobileWorkbenchRoot = ({ routes }: { routes: RouteObject[] }) => {
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
// for debugging
|
||||
(window as any).workbench = workbench;
|
||||
|
||||
const views = useLiveData(workbench.views$);
|
||||
|
||||
const location = useLocation();
|
||||
const basename = location.pathname.match(/\/workspace\/[^/]+/g)?.[0] ?? '/';
|
||||
|
||||
useBindWorkbenchToBrowserRouter(workbench, basename);
|
||||
|
||||
useEffect(() => {
|
||||
workbench.updateBasename(basename);
|
||||
}, [basename, workbench]);
|
||||
|
||||
return <ViewRoot routes={routes} view={views[0]} />;
|
||||
};
|
@ -11,7 +11,7 @@ import {
|
||||
|
||||
export const NavigateContext = createContext<NavigateFunction | null>(null);
|
||||
|
||||
function RootRouter() {
|
||||
export function RootRouter() {
|
||||
const navigate = useNavigate();
|
||||
const [ready, setReady] = useState(false);
|
||||
useEffect(() => {
|
||||
|
28
packages/frontend/mobile/package.json
Normal file
28
packages/frontend/mobile/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@affine/mobile",
|
||||
"version": "0.16.0",
|
||||
"description": "AFFiNE Desktop Web application",
|
||||
"private": true,
|
||||
"browser": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "cross-env DISTRIBUTION=mobile yarn workspace @affine/cli build",
|
||||
"dev": "yarn workspace @affine/cli dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/core": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"core-js": "^3.36.1",
|
||||
"intl-segmenter-polyfill-rs": "^0.1.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine/cli": "workspace:*",
|
||||
"@types/react": "^18.2.75",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"cross-env": "^7.0.3",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
69
packages/frontend/mobile/project.json
Normal file
69
packages/frontend/mobile/project.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "@affine/mobile",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-script",
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": ["tag:infra"],
|
||||
"target": "build",
|
||||
"params": "ignore"
|
||||
},
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"{projectRoot}/**/*",
|
||||
"{workspaceRoot}/tools/**/*",
|
||||
"{workspaceRoot}/packages/frontend/core/**/*",
|
||||
"{workspaceRoot}/packages/**/*",
|
||||
{
|
||||
"env": "BUILD_TYPE"
|
||||
},
|
||||
{
|
||||
"env": "BUILD_TYPE_OVERRIDE"
|
||||
},
|
||||
{
|
||||
"env": "PERFSEE_TOKEN"
|
||||
},
|
||||
{
|
||||
"env": "SENTRY_ORG"
|
||||
},
|
||||
{
|
||||
"env": "SENTRY_PROJECT"
|
||||
},
|
||||
{
|
||||
"env": "SENTRY_AUTH_TOKEN"
|
||||
},
|
||||
{
|
||||
"env": "SENTRY_DSN"
|
||||
},
|
||||
{
|
||||
"env": "DISTRIBUTION"
|
||||
},
|
||||
{
|
||||
"env": "COVERAGE"
|
||||
},
|
||||
{
|
||||
"env": "DISABLE_DEV_OVERLAY"
|
||||
},
|
||||
{
|
||||
"env": "CAPTCHA_SITE_KEY"
|
||||
},
|
||||
{
|
||||
"env": "R2_ACCOUNT_ID"
|
||||
},
|
||||
{
|
||||
"env": "R2_ACCESS_KEY_ID"
|
||||
},
|
||||
{
|
||||
"env": "R2_SECRET_ACCESS_KEY"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"script": "build"
|
||||
},
|
||||
"outputs": ["{projectRoot}/dist"]
|
||||
}
|
||||
}
|
||||
}
|
91
packages/frontend/mobile/src/app.tsx
Normal file
91
packages/frontend/mobile/src/app.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
|
||||
import { NotificationCenter } from '@affine/component';
|
||||
import { AffineContext } from '@affine/component/context';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
|
||||
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
|
||||
import {
|
||||
configureBrowserWorkspaceFlavours,
|
||||
configureIndexedDBWorkspaceEngineStorageProvider,
|
||||
} from '@affine/core/modules/workspace-engine';
|
||||
import {
|
||||
performanceLogger,
|
||||
performanceRenderLogger,
|
||||
} from '@affine/core/shared';
|
||||
import { Telemetry } from '@affine/core/telemetry';
|
||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
||||
import {
|
||||
Framework,
|
||||
FrameworkRoot,
|
||||
getCurrentStore,
|
||||
LifecycleService,
|
||||
} from '@toeverything/infra';
|
||||
import { Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { router } from './router';
|
||||
|
||||
if (!environment.isBrowser && environment.isDebug) {
|
||||
document.body.innerHTML = `<h1 style="color:red;font-size:5rem;text-align:center;">Don't run web entry in electron.</h1>`;
|
||||
throw new Error('Wrong distribution');
|
||||
}
|
||||
|
||||
const future = {
|
||||
v7_startTransition: true,
|
||||
} as const;
|
||||
|
||||
const performanceI18nLogger = performanceLogger.namespace('i18n');
|
||||
|
||||
async function loadLanguage() {
|
||||
performanceI18nLogger.info('start');
|
||||
|
||||
const i18n = createI18n();
|
||||
document.documentElement.lang = i18n.language;
|
||||
|
||||
performanceI18nLogger.info('set up');
|
||||
await setUpLanguage(i18n);
|
||||
performanceI18nLogger.info('done');
|
||||
}
|
||||
|
||||
let languageLoadingPromise: Promise<void> | null = null;
|
||||
|
||||
const framework = new Framework();
|
||||
configureCommonModules(framework);
|
||||
configureBrowserWorkbenchModule(framework);
|
||||
configureLocalStorageStateStorageImpls(framework);
|
||||
configureBrowserWorkspaceFlavours(framework);
|
||||
configureIndexedDBWorkspaceEngineStorageProvider(framework);
|
||||
const frameworkProvider = framework.provider();
|
||||
|
||||
// setup application lifecycle events, and emit application start event
|
||||
window.addEventListener('focus', () => {
|
||||
frameworkProvider.get(LifecycleService).applicationFocus();
|
||||
});
|
||||
frameworkProvider.get(LifecycleService).applicationStart();
|
||||
|
||||
export function App() {
|
||||
performanceRenderLogger.debug('App');
|
||||
|
||||
if (!languageLoadingPromise) {
|
||||
languageLoadingPromise = loadLanguage().catch(console.error);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<FrameworkRoot framework={frameworkProvider}>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<Telemetry />
|
||||
<NotificationCenter />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</AffineContext>
|
||||
</FrameworkRoot>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
76
packages/frontend/mobile/src/index.tsx
Normal file
76
packages/frontend/mobile/src/index.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import './polyfill/dispose';
|
||||
import './polyfill/intl-segmenter';
|
||||
import './polyfill/promise-with-resolvers';
|
||||
import './polyfill/request-idle-callback';
|
||||
import '@affine/core/bootstrap/preload';
|
||||
|
||||
import { performanceLogger } from '@affine/core/shared';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import {
|
||||
init,
|
||||
reactRouterV6BrowserTracingIntegration,
|
||||
setTags,
|
||||
} from '@sentry/react';
|
||||
import { StrictMode, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { App } from './app';
|
||||
|
||||
const performanceMainLogger = performanceLogger.namespace('main');
|
||||
function main() {
|
||||
performanceMainLogger.info('start');
|
||||
|
||||
// skip bootstrap setup for desktop onboarding
|
||||
if (isDesktop && window.appInfo?.windowName === 'onboarding') {
|
||||
performanceMainLogger.info('skip setup');
|
||||
} else {
|
||||
performanceMainLogger.info('setup start');
|
||||
if (window.SENTRY_RELEASE || environment.isDebug) {
|
||||
// https://docs.sentry.io/platforms/javascript/guides/react/#configure
|
||||
init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
environment: process.env.BUILD_TYPE ?? 'development',
|
||||
integrations: [
|
||||
reactRouterV6BrowserTracingIntegration({
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
}),
|
||||
],
|
||||
});
|
||||
setTags({
|
||||
appVersion: runtimeConfig.appVersion,
|
||||
editorVersion: runtimeConfig.editorVersion,
|
||||
});
|
||||
}
|
||||
performanceMainLogger.info('setup done');
|
||||
}
|
||||
|
||||
mountApp();
|
||||
}
|
||||
|
||||
function mountApp() {
|
||||
performanceMainLogger.info('import app');
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const root = document.getElementById('app')!;
|
||||
performanceMainLogger.info('render app');
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error('Failed to bootstrap app', err);
|
||||
}
|
3
packages/frontend/mobile/src/pages/404.tsx
Normal file
3
packages/frontend/mobile/src/pages/404.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/404</div>;
|
||||
};
|
3
packages/frontend/mobile/src/pages/auth.tsx
Normal file
3
packages/frontend/mobile/src/pages/auth.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/auth/*</div>;
|
||||
};
|
8
packages/frontend/mobile/src/pages/index.tsx
Normal file
8
packages/frontend/mobile/src/pages/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { Component as IndexComponent } from '@affine/core/pages/index';
|
||||
|
||||
// Default route fallback for mobile
|
||||
|
||||
export const Component = () => {
|
||||
// TODO: replace with a mobile version
|
||||
return <IndexComponent />;
|
||||
};
|
7
packages/frontend/mobile/src/pages/sign-in.tsx
Normal file
7
packages/frontend/mobile/src/pages/sign-in.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
// Default route fallback for mobile
|
||||
import { SignIn } from '@affine/core/pages/sign-in';
|
||||
|
||||
export const Component = () => {
|
||||
// placeholder impl
|
||||
return <SignIn />;
|
||||
};
|
3
packages/frontend/mobile/src/pages/workspace/all.tsx
Normal file
3
packages/frontend/mobile/src/pages/workspace/all.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/workspace/:workspaceId/all</div>;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/workspace/:workspaceId/collection/:collectionId</div>;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/workspace/:workspaceId/collection</div>;
|
||||
};
|
3
packages/frontend/mobile/src/pages/workspace/detail.tsx
Normal file
3
packages/frontend/mobile/src/pages/workspace/detail.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/workspace/:workspaceId/:pageId</div>;
|
||||
};
|
112
packages/frontend/mobile/src/pages/workspace/index.tsx
Normal file
112
packages/frontend/mobile/src/pages/workspace/index.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { RouteContainer } from '@affine/core/modules/workbench/view/route-container';
|
||||
import { PageNotFound } from '@affine/core/pages/404';
|
||||
import { MobileWorkbenchRoot } from '@affine/core/pages/workspace/workbench-root';
|
||||
import {
|
||||
useLiveData,
|
||||
useServices,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { lazy as reactLazy, useEffect, useMemo, useState } from 'react';
|
||||
import { matchPath, useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { viewRoutes } from '../../router';
|
||||
import { WorkspaceLayout } from './layout';
|
||||
|
||||
const warpedRoutes = viewRoutes.map(({ path, lazy }) => {
|
||||
const Component = reactLazy(() =>
|
||||
lazy().then(m => ({
|
||||
default: m.Component as React.ComponentType,
|
||||
}))
|
||||
);
|
||||
const route = {
|
||||
Component,
|
||||
};
|
||||
|
||||
return {
|
||||
path,
|
||||
Component: () => {
|
||||
return <RouteContainer route={route} />;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const Component = () => {
|
||||
const { workspacesService } = useServices({
|
||||
WorkspacesService,
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
|
||||
// todo(pengx17): dedupe the code with core
|
||||
// check if we are in detail doc route, if so, maybe render share page
|
||||
const detailDocRoute = useMemo(() => {
|
||||
const match = matchPath(
|
||||
'/workspace/:workspaceId/:docId',
|
||||
location.pathname
|
||||
);
|
||||
if (
|
||||
match &&
|
||||
match.params.docId &&
|
||||
match.params.workspaceId &&
|
||||
// TODO(eyhn): need a better way to check if it's a docId
|
||||
viewRoutes.find(route => matchPath(route.path, '/' + match.params.docId))
|
||||
?.path === '/:pageId'
|
||||
) {
|
||||
return {
|
||||
docId: match.params.docId,
|
||||
workspaceId: match.params.workspaceId,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const [workspaceNotFound, setWorkspaceNotFound] = useState(false);
|
||||
const listLoading = useLiveData(workspacesService.list.isRevalidating$);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const meta = useMemo(() => {
|
||||
return workspaces.find(({ id }) => id === params.workspaceId);
|
||||
}, [workspaces, params.workspaceId]);
|
||||
|
||||
// if listLoading is false, we can show 404 page, otherwise we should show loading page.
|
||||
useEffect(() => {
|
||||
if (listLoading === false && meta === undefined) {
|
||||
setWorkspaceNotFound(true);
|
||||
}
|
||||
if (meta) {
|
||||
setWorkspaceNotFound(false);
|
||||
}
|
||||
}, [listLoading, meta, workspacesService]);
|
||||
|
||||
// if workspace is not found, we should revalidate in interval
|
||||
useEffect(() => {
|
||||
if (listLoading === false && meta === undefined) {
|
||||
const timer = setInterval(
|
||||
() => workspacesService.list.revalidate(),
|
||||
5000
|
||||
);
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
return;
|
||||
}, [listLoading, meta, workspaceNotFound, workspacesService]);
|
||||
|
||||
if (workspaceNotFound) {
|
||||
if (
|
||||
detailDocRoute /* */ &&
|
||||
environment.isBrowser /* only browser has share page */
|
||||
) {
|
||||
return <div>TODO: share page</div>;
|
||||
}
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
if (!meta) {
|
||||
return <AppFallback key="workspaceLoading" />;
|
||||
}
|
||||
return (
|
||||
<WorkspaceLayout meta={meta}>
|
||||
<MobileWorkbenchRoot routes={warpedRoutes} />
|
||||
</WorkspaceLayout>
|
||||
);
|
||||
};
|
92
packages/frontend/mobile/src/pages/workspace/layout.tsx
Normal file
92
packages/frontend/mobile/src/pages/workspace/layout.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { WorkspaceLayoutProviders } from '@affine/core/layouts/workspace-layout';
|
||||
import {
|
||||
AllWorkspaceModals,
|
||||
CurrentWorkspaceModals,
|
||||
} from '@affine/core/providers/modal-provider';
|
||||
import { SWRConfigProvider } from '@affine/core/providers/swr-config-provider';
|
||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
FrameworkScope,
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
export const WorkspaceLayout = ({
|
||||
meta,
|
||||
children,
|
||||
}: PropsWithChildren<{ meta: WorkspaceMetadata }>) => {
|
||||
// todo: reduce code duplication with packages\frontend\core\src\pages\workspace\index.tsx
|
||||
const { workspacesService, globalContextService } = useServices({
|
||||
WorkspacesService,
|
||||
GlobalContextService,
|
||||
});
|
||||
|
||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ref = workspacesService.open({ metadata: meta });
|
||||
setWorkspace(ref.workspace);
|
||||
return () => {
|
||||
ref.dispose();
|
||||
};
|
||||
}, [meta, workspacesService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
// for debug purpose
|
||||
window.currentWorkspace = workspace ?? undefined;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine:workspace:change', {
|
||||
detail: {
|
||||
id: workspace.id,
|
||||
},
|
||||
})
|
||||
);
|
||||
localStorage.setItem('last_workspace_id', workspace.id);
|
||||
globalContextService.globalContext.workspaceId.set(workspace.id);
|
||||
return () => {
|
||||
window.currentWorkspace = undefined;
|
||||
globalContextService.globalContext.workspaceId.set(null);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [globalContextService, workspace]);
|
||||
|
||||
const isRootDocReady =
|
||||
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
|
||||
|
||||
if (!workspace) {
|
||||
return null; // skip this, workspace will be set in layout effect
|
||||
}
|
||||
|
||||
if (!isRootDocReady) {
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AppFallback key="workspaceLoading" />
|
||||
<AllWorkspaceModals />
|
||||
</FrameworkScope>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<SWRConfigProvider>
|
||||
<AllWorkspaceModals />
|
||||
<CurrentWorkspaceModals />
|
||||
<WorkspaceLayoutProviders>{children}</WorkspaceLayoutProviders>
|
||||
</SWRConfigProvider>
|
||||
</AffineErrorBoundary>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/workspace/:workspaceId/tag/:tagId</div>;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/workspace/:workspaceId/tag</div>;
|
||||
};
|
3
packages/frontend/mobile/src/pages/workspace/trash.tsx
Normal file
3
packages/frontend/mobile/src/pages/workspace/trash.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/workspace/:workspaceId/trash</div>;
|
||||
};
|
2
packages/frontend/mobile/src/polyfill/dispose.ts
Normal file
2
packages/frontend/mobile/src/polyfill/dispose.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import 'core-js/modules/esnext.symbol.async-dispose';
|
||||
import 'core-js/modules/esnext.symbol.dispose';
|
11
packages/frontend/mobile/src/polyfill/intl-segmenter.ts
Normal file
11
packages/frontend/mobile/src/polyfill/intl-segmenter.ts
Normal file
@ -0,0 +1,11 @@
|
||||
if (Intl.Segmenter === undefined) {
|
||||
await import('intl-segmenter-polyfill-rs').then(({ Segmenter }) => {
|
||||
Object.defineProperty(Intl, 'Segmenter', {
|
||||
value: Segmenter,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export {};
|
@ -0,0 +1 @@
|
||||
import 'core-js/features/promise/with-resolvers';
|
@ -0,0 +1,19 @@
|
||||
window.requestIdleCallback =
|
||||
window.requestIdleCallback ||
|
||||
function (cb) {
|
||||
const start = Date.now();
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, 50 - (Date.now() - start));
|
||||
},
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
|
||||
window.cancelIdleCallback =
|
||||
window.cancelIdleCallback ||
|
||||
function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
1
packages/frontend/mobile/src/polyfill/set-immediate.ts
Normal file
1
packages/frontend/mobile/src/polyfill/set-immediate.ts
Normal file
@ -0,0 +1 @@
|
||||
import 'setimmediate';
|
95
packages/frontend/mobile/src/router.tsx
Normal file
95
packages/frontend/mobile/src/router.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { RootRouter } from '@affine/core/router';
|
||||
import { wrapCreateBrowserRouter } from '@sentry/react';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import {
|
||||
createBrowserRouter as reactRouterCreateBrowserRouter,
|
||||
redirect,
|
||||
} from 'react-router-dom';
|
||||
|
||||
export const topLevelRoutes = [
|
||||
{
|
||||
element: <RootRouter />,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
lazy: () => import('./pages/index'),
|
||||
},
|
||||
{
|
||||
path: '/workspace/:workspaceId/*',
|
||||
lazy: () => import('./pages/workspace/index'),
|
||||
},
|
||||
{
|
||||
path: '/share/:workspaceId/:pageId',
|
||||
loader: ({ params }) => {
|
||||
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
lazy: () => import('./pages/404'),
|
||||
},
|
||||
{
|
||||
path: '/auth/:authType',
|
||||
lazy: () => import('./pages/auth'),
|
||||
},
|
||||
{
|
||||
path: '/sign-in',
|
||||
lazy: () => import('./pages/sign-in'),
|
||||
},
|
||||
{
|
||||
path: '/redirect-proxy',
|
||||
lazy: () => import('@affine/core/pages/redirect'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
lazy: () => import('./pages/404'),
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies [RouteObject, ...RouteObject[]];
|
||||
|
||||
export const viewRoutes = [
|
||||
{
|
||||
path: '/all',
|
||||
lazy: () => import('./pages/workspace/all'),
|
||||
},
|
||||
{
|
||||
path: '/collection',
|
||||
lazy: () => import('./pages/workspace/collection/index'),
|
||||
},
|
||||
{
|
||||
path: '/collection/:collectionId',
|
||||
lazy: () => import('./pages/workspace/collection/detail'),
|
||||
},
|
||||
{
|
||||
path: '/tag',
|
||||
lazy: () => import('./pages/workspace/tag/index'),
|
||||
},
|
||||
{
|
||||
path: '/tag/:tagId',
|
||||
lazy: () => import('./pages/workspace/tag/detail'),
|
||||
},
|
||||
{
|
||||
path: '/trash',
|
||||
lazy: () => import('./pages/workspace/trash'),
|
||||
},
|
||||
{
|
||||
path: '/:pageId',
|
||||
lazy: () => import('./pages/workspace/detail'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
lazy: () => import('./pages/404'),
|
||||
},
|
||||
] satisfies [RouteObject, ...RouteObject[]];
|
||||
|
||||
const createBrowserRouter = wrapCreateBrowserRouter(
|
||||
reactRouterCreateBrowserRouter
|
||||
);
|
||||
export const router = (
|
||||
window.SENTRY_RELEASE ? createBrowserRouter : reactRouterCreateBrowserRouter
|
||||
)(topLevelRoutes, {
|
||||
future: {
|
||||
v7_normalizeFormMethod: true,
|
||||
},
|
||||
});
|
12
packages/frontend/mobile/tsconfig.json
Normal file
12
packages/frontend/mobile/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "lib",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["affine__env"],
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [{ "path": "../core" }]
|
||||
}
|
@ -49,6 +49,9 @@ const buildFlags = process.argv.includes('--static')
|
||||
{
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
value: 'mobile',
|
||||
},
|
||||
],
|
||||
initialValue: 'browser',
|
||||
}),
|
||||
|
@ -23,6 +23,8 @@ module.exports.getCwdFromDistribution = function getCwdFromDistribution(
|
||||
return join(projectRoot, 'packages/frontend/electron/renderer');
|
||||
case 'admin':
|
||||
return join(projectRoot, 'packages/frontend/admin');
|
||||
case 'mobile':
|
||||
return join(projectRoot, 'packages/frontend/mobile');
|
||||
default: {
|
||||
throw new Error('DISTRIBUTION must be one of browser, desktop');
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export type BuildFlags = {
|
||||
distribution: 'browser' | 'desktop' | 'admin';
|
||||
distribution: 'browser' | 'desktop' | 'admin' | 'mobile';
|
||||
mode: 'development' | 'production';
|
||||
channel: 'stable' | 'beta' | 'canary' | 'internal';
|
||||
coverage?: boolean;
|
||||
|
20
yarn.lock
20
yarn.lock
@ -637,6 +637,26 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/mobile@workspace:packages/frontend/mobile":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/mobile@workspace:packages/frontend/mobile"
|
||||
dependencies:
|
||||
"@affine/cli": "workspace:*"
|
||||
"@affine/component": "workspace:*"
|
||||
"@affine/core": "workspace:*"
|
||||
"@affine/env": "workspace:*"
|
||||
"@sentry/react": "npm:^8.0.0"
|
||||
"@types/react": "npm:^18.2.75"
|
||||
"@types/react-dom": "npm:^18.2.24"
|
||||
core-js: "npm:^3.36.1"
|
||||
cross-env: "npm:^7.0.3"
|
||||
intl-segmenter-polyfill-rs: "npm:^0.1.7"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
typescript: "npm:^5.4.5"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/monorepo@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/monorepo@workspace:."
|
||||
|
Loading…
Reference in New Issue
Block a user