feat: init mobile entry (#7905)
This commit is contained in:
@@ -73,7 +73,7 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
|
export const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const pushGlobalLoadingEvent = useSetAtom(pushGlobalLoadingEventAtom);
|
const pushGlobalLoadingEvent = useSetAtom(pushGlobalLoadingEventAtom);
|
||||||
const resolveGlobalLoadingEvent = useSetAtom(resolveGlobalLoadingEventAtom);
|
const resolveGlobalLoadingEvent = useSetAtom(resolveGlobalLoadingEventAtom);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FrameworkScope, useLiveData } from '@toeverything/infra';
|
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 {
|
import {
|
||||||
createMemoryRouter,
|
createMemoryRouter,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
@@ -7,30 +8,16 @@ import {
|
|||||||
UNSAFE_RouteContext,
|
UNSAFE_RouteContext,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { viewRoutes } from '../../../router';
|
|
||||||
import type { View } from '../entities/view';
|
import type { View } from '../entities/view';
|
||||||
import { RouteContainer } from './route-container';
|
|
||||||
|
|
||||||
const warpedRoutes = viewRoutes.map(({ path, lazy }) => {
|
export const ViewRoot = ({
|
||||||
const Component = reactLazy(() =>
|
view,
|
||||||
lazy().then(m => ({
|
routes,
|
||||||
default: m.Component as React.ComponentType,
|
}: {
|
||||||
}))
|
view: View;
|
||||||
);
|
routes: RouteObject[];
|
||||||
const route = {
|
}) => {
|
||||||
Component,
|
const viewRouter = useMemo(() => createMemoryRouter(routes), [routes]);
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
Component: () => {
|
|
||||||
return <RouteContainer route={route} />;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ViewRoot = ({ view }: { view: View }) => {
|
|
||||||
const viewRouter = useMemo(() => createMemoryRouter(warpedRoutes), []);
|
|
||||||
|
|
||||||
const location = useLiveData(view.location$);
|
const location = useLiveData(view.location$);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ResizePanel } from '@affine/component/resize-panel';
|
import { ResizePanel } from '@affine/component/resize-panel';
|
||||||
import { rightSidebarWidthAtom } from '@affine/core/atoms';
|
import { rightSidebarWidthAtom } from '@affine/core/atoms';
|
||||||
|
import { viewRoutes } from '@affine/core/router';
|
||||||
import {
|
import {
|
||||||
appSettingAtom,
|
appSettingAtom,
|
||||||
FrameworkScope,
|
FrameworkScope,
|
||||||
@@ -7,13 +8,21 @@ import {
|
|||||||
useService,
|
useService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
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 { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import type { View } from '../entities/view';
|
import type { View } from '../entities/view';
|
||||||
import { WorkbenchService } from '../services/workbench';
|
import { WorkbenchService } from '../services/workbench';
|
||||||
import { useBindWorkbenchToBrowserRouter } from './browser-adapter';
|
import { useBindWorkbenchToBrowserRouter } from './browser-adapter';
|
||||||
import { useBindWorkbenchToDesktopRouter } from './desktop-adapter';
|
import { useBindWorkbenchToDesktopRouter } from './desktop-adapter';
|
||||||
|
import { RouteContainer } from './route-container';
|
||||||
import { SidebarContainer } from './sidebar/sidebar-container';
|
import { SidebarContainer } from './sidebar/sidebar-container';
|
||||||
import { SplitView } from './split-view/split-view';
|
import { SplitView } from './split-view/split-view';
|
||||||
import { ViewIslandRegistryProvider } from './view-islands';
|
import { ViewIslandRegistryProvider } from './view-islands';
|
||||||
@@ -24,6 +33,24 @@ const useAdapter = environment.isDesktop
|
|||||||
? useBindWorkbenchToDesktopRouter
|
? useBindWorkbenchToDesktopRouter
|
||||||
: useBindWorkbenchToBrowserRouter;
|
: 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(() => {
|
export const WorkbenchRoot = memo(() => {
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|
||||||
@@ -93,7 +120,7 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.workbenchViewContainer} ref={containerRef}>
|
<div className={styles.workbenchViewContainer} ref={containerRef}>
|
||||||
<ViewRoot key={view.id} view={view} />
|
<ViewRoot routes={warpedRoutes} key={view.id} view={view} />
|
||||||
</div>
|
</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);
|
export const NavigateContext = createContext<NavigateFunction | null>(null);
|
||||||
|
|
||||||
function RootRouter() {
|
export function RootRouter() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
useEffect(() => {
|
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: 'admin',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'mobile',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
initialValue: 'browser',
|
initialValue: 'browser',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ module.exports.getCwdFromDistribution = function getCwdFromDistribution(
|
|||||||
return join(projectRoot, 'packages/frontend/electron/renderer');
|
return join(projectRoot, 'packages/frontend/electron/renderer');
|
||||||
case 'admin':
|
case 'admin':
|
||||||
return join(projectRoot, 'packages/frontend/admin');
|
return join(projectRoot, 'packages/frontend/admin');
|
||||||
|
case 'mobile':
|
||||||
|
return join(projectRoot, 'packages/frontend/mobile');
|
||||||
default: {
|
default: {
|
||||||
throw new Error('DISTRIBUTION must be one of browser, desktop');
|
throw new Error('DISTRIBUTION must be one of browser, desktop');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type BuildFlags = {
|
export type BuildFlags = {
|
||||||
distribution: 'browser' | 'desktop' | 'admin';
|
distribution: 'browser' | 'desktop' | 'admin' | 'mobile';
|
||||||
mode: 'development' | 'production';
|
mode: 'development' | 'production';
|
||||||
channel: 'stable' | 'beta' | 'canary' | 'internal';
|
channel: 'stable' | 'beta' | 'canary' | 'internal';
|
||||||
coverage?: boolean;
|
coverage?: boolean;
|
||||||
|
|||||||
20
yarn.lock
20
yarn.lock
@@ -637,6 +637,26 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
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:.":
|
"@affine/monorepo@workspace:.":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/monorepo@workspace:."
|
resolution: "@affine/monorepo@workspace:."
|
||||||
|
|||||||
Reference in New Issue
Block a user