feat(infra): framework

This commit is contained in:
EYHN
2024-04-17 14:12:29 +08:00
parent ab17a05df3
commit 06fda3b62c
467 changed files with 9996 additions and 8697 deletions
+9 -9
View File
@@ -2,14 +2,14 @@ import {
NoPermissionOrNotFound,
NotFoundPage,
} from '@affine/component/not-found-page';
import { useSession } from '@affine/core/hooks/affine/use-current-user';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useLiveData, useService } from '@toeverything/infra';
import type { ReactElement } from 'react';
import { useCallback, useState } from 'react';
import { SignOutModal } from '../components/affine/sign-out-modal';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { signOutCloud } from '../utils/cloud-utils';
import { AuthService } from '../modules/cloud';
import { SignIn } from './sign-in';
export const PageNotFound = ({
@@ -17,9 +17,9 @@ export const PageNotFound = ({
}: {
noPermission?: boolean;
}): ReactElement => {
const { user } = useSession();
const authService = useService(AuthService);
const account = useLiveData(authService.session.account$);
const { jumpToIndex } = useNavigateHelper();
const { reload } = useSession();
const [open, setOpen] = useState(false);
const handleBackButtonClick = useCallback(
@@ -33,21 +33,21 @@ export const PageNotFound = ({
const onConfirmSignOut = useAsyncCallback(async () => {
setOpen(false);
await signOutCloud();
await reload();
}, [reload]);
await authService.signOut();
}, [authService]);
return (
<>
{noPermission ? (
<NoPermissionOrNotFound
user={user}
user={account}
onBack={handleBackButtonClick}
onSignOut={handleOpenSignOutModal}
signInComponent={<SignIn />}
/>
) : (
<NotFoundPage
user={user}
user={account}
onBack={handleBackButtonClick}
onSignOut={handleOpenSignOutModal}
/>
+27 -16
View File
@@ -8,7 +8,6 @@ import {
SignInSuccessPage,
SignUpPage,
} from '@affine/component/auth-components';
import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config';
import {
changeEmailMutation,
changePasswordMutation,
@@ -17,18 +16,17 @@ import {
verifyEmailMutation,
} from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useLiveData, useService } from '@toeverything/infra';
import type { ReactElement } from 'react';
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect, useParams, useSearchParams } from 'react-router-dom';
import { z } from 'zod';
import { SubscriptionRedirect } from '../components/affine/auth/subscription-redirect';
import { WindowsAppControls } from '../components/pure/header/windows-app-controls';
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
import { useCurrentUser } from '../hooks/affine/use-current-user';
import { useMutation } from '../hooks/use-mutation';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { AuthService, ServerConfigService } from '../modules/cloud';
const authTypeSchema = z.enum([
'onboarding',
@@ -43,9 +41,13 @@ const authTypeSchema = z.enum([
]);
export const AuthPage = (): ReactElement | null => {
const user = useCurrentUser();
const authService = useService(AuthService);
const account = useLiveData(authService.session.account$);
const t = useAFFiNEI18N();
const { password: passwordLimits } = useCredentialsRequirement();
const serverConfig = useService(ServerConfigService).serverConfig;
const passwordLimits = useLiveData(
serverConfig.credentialsRequirement$.map(r => r?.password)
);
const { authType } = useParams();
const [searchParams] = useSearchParams();
@@ -97,11 +99,16 @@ export const AuthPage = (): ReactElement | null => {
jumpToIndex(RouteLogic.REPLACE);
}, [jumpToIndex]);
if (!passwordLimits || !account) {
// TODO: loading UI
return null;
}
switch (authType) {
case 'onboarding':
return (
<OnboardingPage
user={user}
user={account}
onOpenAffine={onOpenAffine}
windowControl={<WindowsAppControls />}
/>
@@ -109,7 +116,7 @@ export const AuthPage = (): ReactElement | null => {
case 'signUp': {
return (
<SignUpPage
user={user}
user={account}
passwordLimits={passwordLimits}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
@@ -122,7 +129,7 @@ export const AuthPage = (): ReactElement | null => {
case 'changePassword': {
return (
<ChangePasswordPage
user={user}
user={account}
passwordLimits={passwordLimits}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
@@ -132,7 +139,7 @@ export const AuthPage = (): ReactElement | null => {
case 'setPassword': {
return (
<SetPasswordPage
user={user}
user={account}
passwordLimits={passwordLimits}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
@@ -150,9 +157,6 @@ export const AuthPage = (): ReactElement | null => {
case 'confirm-change-email': {
return <ConfirmChangeEmail onOpenAffine={onOpenAffine} />;
}
case 'subscription-redirect': {
return <SubscriptionRedirect />;
}
case 'verify-email': {
return <ConfirmChangeEmail onOpenAffine={onOpenAffine} />;
}
@@ -203,10 +207,16 @@ export const loader: LoaderFunction = async args => {
};
export const Component = () => {
const loginStatus = useCurrentLoginStatus();
const authService = useService(AuthService);
const isRevalidating = useLiveData(authService.session.isRevalidating$);
const loginStatus = useLiveData(authService.session.status$);
const { jumpToExpired } = useNavigateHelper();
if (loginStatus === 'unauthenticated') {
useEffect(() => {
authService.session.revalidate();
}, [authService]);
if (loginStatus === 'unauthenticated' && !isRevalidating) {
jumpToExpired(RouteLogic.REPLACE);
}
@@ -214,5 +224,6 @@ export const Component = () => {
return <AuthPage />;
}
// TODO: loading UI
return null;
};
@@ -2,9 +2,6 @@ import { OAuthProviderType } from '@affine/graphql';
import type { LoaderFunction } from 'react-router-dom';
import { z } from 'zod';
import { getSession } from '../hooks/affine/use-current-user';
import { signInCloud, signOutCloud } from '../utils/cloud-utils';
const supportedProvider = z.enum([
'google',
...Object.values(OAuthProviderType),
@@ -22,12 +19,8 @@ export const loader: LoaderFunction = async ({ request }) => {
return null;
}
const session = await getSession();
if (session.user) {
// already signed in, need to sign out first
await signOutCloud(request.url);
}
// sign out first
await fetch('/api/auth/sign-out');
const maybeProvider = supportedProvider.safeParse(provider);
if (maybeProvider.success) {
@@ -36,9 +29,11 @@ export const loader: LoaderFunction = async ({ request }) => {
if (provider === 'google') {
provider = OAuthProviderType.Google;
}
await signInCloud(provider, undefined, {
redirectUri,
});
location.href = `${
runtimeConfig.serverUrlPrefix
}/oauth/login?provider=${provider}&redirect_uri=${encodeURIComponent(
redirectUri
)}`;
}
return null;
};
+26 -22
View File
@@ -4,8 +4,7 @@ import {
initEmptyPage,
useLiveData,
useService,
WorkspaceListService,
WorkspaceManager,
WorkspacesService,
} from '@toeverything/infra';
import {
lazy,
@@ -20,8 +19,8 @@ import { type LoaderFunction, useSearchParams } from 'react-router-dom';
import { createFirstAppData } from '../bootstrap/first-app-data';
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
import { WorkspaceFallback } from '../components/workspace';
import { useSession } from '../hooks/affine/use-current-user';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { AuthService } from '../modules/cloud';
import { WorkspaceSubPath } from '../shared';
const AllWorkspaceModals = lazy(() =>
@@ -36,14 +35,16 @@ export const loader: LoaderFunction = async () => {
export const Component = () => {
// navigating and creating may be slow, to avoid flickering, we show workspace fallback
const [navigating, setNavigating] = useState(false);
const [navigating, setNavigating] = useState(true);
const [creating, setCreating] = useState(false);
const { status } = useSession();
const workspaceManager = useService(WorkspaceManager);
const authService = useService(AuthService);
const loggedIn = useLiveData(
authService.session.status$.map(s => s === 'authenticated')
);
const workspaceListService = useService(WorkspaceListService);
const list = useLiveData(workspaceListService.workspaceList$);
const workspaceListStatus = useLiveData(workspaceListService.status$);
const workspacesService = useService(WorkspacesService);
const list = useLiveData(workspacesService.list.workspaces$);
const listIsLoading = useLiveData(workspacesService.list.isLoading$);
const { openPage } = useNavigateHelper();
const [searchParams] = useSearchParams();
@@ -53,26 +54,23 @@ export const Component = () => {
const createCloudWorkspace = useCallback(() => {
if (createOnceRef.current) return;
createOnceRef.current = true;
workspaceManager
.createWorkspace(WorkspaceFlavour.AFFINE_CLOUD, async workspace => {
workspacesService
.create(WorkspaceFlavour.AFFINE_CLOUD, async workspace => {
workspace.meta.setName('AFFiNE Cloud');
const page = workspace.createDoc();
initEmptyPage(page);
})
.then(workspace => openPage(workspace.id, WorkspaceSubPath.ALL))
.catch(err => console.error('Failed to create cloud workspace', err));
}, [openPage, workspaceManager]);
}, [openPage, workspacesService]);
useLayoutEffect(() => {
if (workspaceListStatus.loading) {
if (listIsLoading) {
return;
}
// check is user logged in && has cloud workspace
if (
searchParams.get('initCloud') === 'true' &&
status === 'authenticated'
) {
if (searchParams.get('initCloud') === 'true' && loggedIn) {
searchParams.delete('initCloud');
if (list.every(w => w.flavour !== WorkspaceFlavour.AFFINE_CLOUD)) {
createCloudWorkspace();
@@ -81,6 +79,7 @@ export const Component = () => {
}
if (list.length === 0) {
setNavigating(false);
return;
}
@@ -89,26 +88,31 @@ export const Component = () => {
const openWorkspace = list.find(w => w.id === lastId) ?? list[0];
openPage(openWorkspace.id, WorkspaceSubPath.ALL);
setNavigating(true);
}, [
createCloudWorkspace,
list,
openPage,
searchParams,
status,
workspaceListStatus.loading,
listIsLoading,
loggedIn,
navigating,
]);
useEffect(() => {
setCreating(true);
createFirstAppData(workspaceManager)
createFirstAppData(workspacesService)
.then(workspaceMeta => {
if (workspaceMeta) {
openPage(workspaceMeta.id, WorkspaceSubPath.ALL);
}
})
.catch(err => {
console.error('Failed to create first app data', err);
})
.finally(() => {
setCreating(false);
});
}, [workspaceManager]);
}, [openPage, workspacesService]);
if (navigating || creating) {
return <WorkspaceFallback></WorkspaceFallback>;
+12 -3
View File
@@ -6,6 +6,7 @@ import {
fetcher,
getInviteInfoQuery,
} from '@affine/graphql';
import { useLiveData, useService } from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import type { LoaderFunction } from 'react-router-dom';
@@ -13,8 +14,8 @@ import { redirect, useLoaderData } from 'react-router-dom';
import { authAtom } from '../atoms';
import { setOnceSignedInEventAtom } from '../atoms/event';
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { AuthService } from '../modules/cloud';
export const loader: LoaderFunction = async args => {
const inviteId = args.params.inviteId || '';
@@ -47,7 +48,14 @@ export const loader: LoaderFunction = async args => {
};
export const Component = () => {
const loginStatus = useCurrentLoginStatus();
const authService = useService(AuthService);
const isRevalidating = useLiveData(authService.session.isRevalidating$);
const loginStatus = useLiveData(authService.session.status$);
useEffect(() => {
authService.session.revalidate();
}, [authService]);
const { jumpToSignIn } = useNavigateHelper();
const { jumpToSubPath } = useNavigateHelper();
@@ -68,7 +76,7 @@ export const Component = () => {
}, [inviteInfo.workspace.id, jumpToSubPath]);
useEffect(() => {
if (loginStatus === 'unauthenticated') {
if (loginStatus === 'unauthenticated' && !isRevalidating) {
// We can not pass function to navigate state, so we need to save it in atom
setOnceSignedInEvent(openWorkspace);
jumpToSignIn(RouteLogic.REPLACE, {
@@ -79,6 +87,7 @@ export const Component = () => {
}
}, [
inviteInfo.workspace.id,
isRevalidating,
jumpToSignIn,
loginStatus,
openWorkspace,
@@ -1,32 +1,23 @@
import { Scrollable } from '@affine/component';
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
import { AuthService } from '@affine/core/modules/cloud';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { fetchWithTraceReport } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
AffineCloudBlobStorage,
StaticBlobStorage,
} from '@affine/workspace-impl';
import { noop } from '@blocksuite/global/utils';
import { Logo1Icon } from '@blocksuite/icons';
import type { AffineEditorContainer } from '@blocksuite/presets';
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
import type { Doc, PageMode } from '@toeverything/infra';
import type { Doc, DocMode, Workspace } from '@toeverything/infra';
import {
DocStorageImpl,
DocsService,
EmptyBlobStorage,
LocalBlobStorage,
PageManager,
FrameworkScope,
ReadonlyDocStorage,
RemoteBlobStorage,
ServiceProviderContext,
useLiveData,
useService,
WorkspaceIdContext,
WorkspaceManager,
WorkspaceScope,
WorkspaceFlavourProvider,
WorkspacesService,
} from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback, useEffect, useState } from 'react';
@@ -42,7 +33,7 @@ import { AppContainer } from '../../components/affine/app-container';
import { PageDetailEditor } from '../../components/page-detail-editor';
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
import { MainContainer } from '../../components/workspace';
import { CurrentWorkspaceService } from '../../modules/workspace';
import { CloudBlobStorage } from '../../modules/workspace-engine/impls/engine/blob-cloud';
import * as styles from './share-detail-page.css';
import { ShareFooter } from './share-footer';
import { ShareHeader } from './share-header';
@@ -58,12 +49,7 @@ export async function downloadBinaryFromCloud(
rootGuid: string,
pageGuid: string
): Promise<CloudDoc | null> {
const response = await fetchWithTraceReport(
`/api/workspaces/${rootGuid}/docs/${pageGuid}`,
{
priority: 'high',
}
);
const response = await fetch(`/api/workspaces/${rootGuid}/docs/${pageGuid}`);
if (response.ok) {
const publishMode = (response.headers.get('publish-mode') ||
'page') as DocPublishMode;
@@ -79,7 +65,7 @@ export async function downloadBinaryFromCloud(
type LoaderData = {
pageId: string;
workspaceId: string;
publishMode: PageMode;
publishMode: DocMode;
pageArrayBuffer: ArrayBuffer;
workspaceArrayBuffer: ArrayBuffer;
};
@@ -124,72 +110,90 @@ export const loader: LoaderFunction = async ({ params }) => {
export const Component = () => {
const {
workspaceId,
pageId,
pageId: docId,
publishMode,
workspaceArrayBuffer,
pageArrayBuffer,
} = useLoaderData() as LoaderData;
const workspaceManager = useService(WorkspaceManager);
const workspacesService = useService(WorkspacesService);
const currentWorkspace = useService(CurrentWorkspaceService);
const t = useAFFiNEI18N();
const [workspace, setWorkspace] = useState<Workspace | null>(null);
const [page, setPage] = useState<Doc | null>(null);
const [_, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor();
const defaultCloudProvider = workspacesService.framework.get(
WorkspaceFlavourProvider('CLOUD')
);
useEffect(() => {
// create a workspace for share page
const workspace = workspaceManager.instantiate(
const { workspace } = workspacesService.open(
{
id: workspaceId,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
metadata: {
id: workspaceId,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
},
isSharedMode: true,
},
services => {
services
.scope(WorkspaceScope)
.addImpl(LocalBlobStorage, EmptyBlobStorage)
.addImpl(RemoteBlobStorage('affine'), AffineCloudBlobStorage, [
WorkspaceIdContext,
])
.addImpl(RemoteBlobStorage('static'), StaticBlobStorage)
.addImpl(
DocStorageImpl,
new ReadonlyDocStorage({
[workspaceId]: new Uint8Array(workspaceArrayBuffer),
[pageId]: new Uint8Array(pageArrayBuffer),
})
);
{
...defaultCloudProvider,
getEngineProvider(workspace) {
return {
getDocStorage() {
return new ReadonlyDocStorage({
[workspace.id]: new Uint8Array(workspaceArrayBuffer),
[docId]: new Uint8Array(pageArrayBuffer),
});
},
getAwarenessConnections() {
return [];
},
getDocServer() {
return null;
},
getLocalBlobStorage() {
return EmptyBlobStorage;
},
getRemoteBlobStorages() {
return [new CloudBlobStorage(workspace.id)];
},
};
},
}
);
setWorkspace(workspace);
workspace.engine
.waitForRootDocReady()
.then(() => {
const { page } = workspace.services.get(PageManager).open(pageId);
const { doc } = workspace.scope.get(DocsService).open(docId);
workspace.docCollection.awarenessStore.setReadonly(
page.blockSuiteDoc.blockCollection,
doc.blockSuiteDoc.blockCollection,
true
);
currentWorkspace.openWorkspace(workspace);
setPage(page);
setPage(doc);
})
.catch(err => {
console.error(err);
});
}, [
currentWorkspace,
defaultCloudProvider,
pageArrayBuffer,
pageId,
docId,
workspaceArrayBuffer,
workspaceId,
workspaceManager,
workspacesService,
]);
const pageTitle = useLiveData(page?.title$);
usePageDocumentTitle(pageTitle);
const loginStatus = useCurrentLoginStatus();
const authService = useService(AuthService);
const loginStatus = useLiveData(authService.session.status$);
const onEditorLoad = useCallback(
(_: BlockSuiteDoc, editor: AffineEditorContainer) => {
@@ -199,57 +203,59 @@ export const Component = () => {
[setActiveBlocksuiteEditor]
);
if (!page) {
if (!workspace || !page) {
return;
}
return (
<ServiceProviderContext.Provider value={page.services}>
<AppContainer>
<MainContainer>
<div className={styles.root}>
<div className={styles.mainContainer}>
<ShareHeader
pageId={page.id}
publishMode={publishMode}
docCollection={page.blockSuiteDoc.collection}
/>
<Scrollable.Root>
<Scrollable.Viewport
className={clsx(
'affine-page-viewport',
styles.editorContainer
)}
>
<PageDetailEditor
isPublic
publishMode={publishMode}
docCollection={page.blockSuiteDoc.collection}
pageId={page.id}
onLoad={onEditorLoad}
/>
{publishMode === 'page' ? <ShareFooter /> : null}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
{loginStatus !== 'authenticated' ? (
<a
href="https://affine.pro"
target="_blank"
className={styles.link}
rel="noreferrer"
>
<span className={styles.linkText}>
{t['com.affine.share-page.footer.built-with']()}
</span>
<Logo1Icon fontSize={20} />
</a>
) : null}
<FrameworkScope scope={workspace.scope}>
<FrameworkScope scope={page.scope}>
<AppContainer>
<MainContainer>
<div className={styles.root}>
<div className={styles.mainContainer}>
<ShareHeader
pageId={page.id}
publishMode={publishMode}
docCollection={page.blockSuiteDoc.collection}
/>
<Scrollable.Root>
<Scrollable.Viewport
className={clsx(
'affine-page-viewport',
styles.editorContainer
)}
>
<PageDetailEditor
isPublic
publishMode={publishMode}
docCollection={page.blockSuiteDoc.collection}
pageId={page.id}
onLoad={onEditorLoad}
/>
{publishMode === 'page' ? <ShareFooter /> : null}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
{loginStatus !== 'authenticated' ? (
<a
href="https://affine.pro"
target="_blank"
className={styles.link}
rel="noreferrer"
>
<span className={styles.linkText}>
{t['com.affine.share-page.footer.built-with']()}
</span>
<Logo1Icon fontSize={20} />
</a>
) : null}
</div>
</div>
</div>
</MainContainer>
</AppContainer>
</ServiceProviderContext.Provider>
</MainContainer>
</AppContainer>
</FrameworkScope>
</FrameworkScope>
);
};
@@ -1,7 +1,7 @@
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
import type { DocCollection } from '@blocksuite/store';
import type { PageMode } from '@toeverything/infra';
import type { DocMode } from '@toeverything/infra';
import { BlocksuiteHeaderTitle } from '../../components/blocksuite/block-suite-header/title/index';
import * as styles from './share-header.css';
@@ -12,7 +12,7 @@ export function ShareHeader({
docCollection,
}: {
pageId: string;
publishMode: PageMode;
publishMode: DocMode;
docCollection: DocCollection;
}) {
return (
+8 -27
View File
@@ -1,16 +1,15 @@
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
import { SignInPageContainer } from '@affine/component/auth-components';
import { AuthService } from '@affine/core/modules/cloud';
import { useLiveData, useService } from '@toeverything/infra';
import { useAtom } from 'jotai';
import { useCallback, useEffect, useRef } from 'react';
import { useCallback, useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { authAtom } from '../atoms';
import type { AuthProps } from '../components/affine/auth';
import { AuthPanel } from '../components/affine/auth';
import { SubscriptionRedirect } from '../components/affine/auth/subscription-redirect';
import { useSubscriptionSearch } from '../components/affine/auth/use-subscription';
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
interface LocationState {
@@ -19,31 +18,18 @@ interface LocationState {
};
}
export const SignIn = () => {
const paymentRedirectRef = useRef<'redirect' | 'ignore' | null>(null);
const [{ state, email = '', emailType = 'changePassword' }, setAuthAtom] =
useAtom(authAtom);
const loginStatus = useCurrentLoginStatus();
const session = useService(AuthService).session;
const status = useLiveData(session.status$);
const isRevalidating = useLiveData(session.isRevalidating$);
const location = useLocation() as LocationState;
const navigate = useNavigate();
const { jumpToIndex } = useNavigateHelper();
const subscriptionData = useSubscriptionSearch();
const [searchParams] = useSearchParams();
const isLoggedIn = loginStatus === 'authenticated';
// Check payment redirect once after session loaded, to avoid unnecessary page rendering.
if (loginStatus !== 'loading' && !paymentRedirectRef.current) {
// If user is logged in and visit sign in page with subscription query, redirect to stripe payment page immediately.
// Otherwise, user will login through email, and then redirect to payment page.
paymentRedirectRef.current =
subscriptionData && isLoggedIn ? 'redirect' : 'ignore';
}
const isLoggedIn = status === 'authenticated' && !isRevalidating;
useEffect(() => {
if (paymentRedirectRef.current === 'redirect') {
return;
}
if (isLoggedIn) {
if (location.state?.callbackURL) {
navigate(location.state.callbackURL, {
@@ -57,10 +43,9 @@ export const SignIn = () => {
}
}, [
jumpToIndex,
location.state?.callbackURL,
location.state,
navigate,
setAuthAtom,
subscriptionData,
isLoggedIn,
searchParams,
]);
@@ -86,10 +71,6 @@ export const SignIn = () => {
[setAuthAtom]
);
if (paymentRedirectRef.current === 'redirect') {
return <SubscriptionRedirect />;
}
return (
<SignInPageContainer>
<div style={{ maxWidth: '400px', width: '100%' }}>
@@ -8,7 +8,7 @@ import {
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useLiveData, useService, Workspace } from '@toeverything/infra';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { nanoid } from 'nanoid';
import { useCallback, useMemo, useState } from 'react';
@@ -20,7 +20,7 @@ import * as styles from './index.css';
export const AllCollection = () => {
const t = useAFFiNEI18N();
const currentWorkspace = useService(Workspace);
const currentWorkspace = useService(WorkspaceService).workspace;
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
const collectionService = useService(CollectionService);
@@ -1,6 +1,6 @@
import { CollectionService } from '@affine/core/modules/collection';
import type { Collection, Filter } from '@affine/env/filter';
import { useService, Workspace } from '@toeverything/infra';
import { useService, WorkspaceService } from '@toeverything/infra';
import { useCallback } from 'react';
import { filterContainerStyle } from '../../../components/filter-container.css';
@@ -17,7 +17,7 @@ export const FilterContainer = ({
filters: Filter[];
onChangeFilters: (filters: Filter[]) => void;
}) => {
const currentWorkspace = useService(Workspace);
const currentWorkspace = useService(WorkspaceService).workspace;
const navigateHelper = useNavigateHelper();
const collectionService = useService(CollectionService);
const saveToCollection = useCallback(
@@ -8,7 +8,7 @@ import { Header } from '@affine/core/components/pure/header';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import type { Filter } from '@affine/env/filter';
import { PlusIcon } from '@blocksuite/icons';
import { useService, Workspace } from '@toeverything/infra';
import { useService, WorkspaceService } from '@toeverything/infra';
import clsx from 'clsx';
import * as styles from './all-page.css';
@@ -22,10 +22,11 @@ export const AllPageHeader = ({
filters: Filter[];
onChangeFilters: (filters: Filter[]) => void;
}) => {
const workspace = useService(Workspace);
const workspace = useService(WorkspaceService).workspace;
const { importFile, createEdgeless, createPage } = usePageHelper(
workspace.docCollection
);
return (
<Header
left={
@@ -7,7 +7,7 @@ import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-me
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { performanceRenderLogger } from '@affine/core/shared';
import type { Filter } from '@affine/env/filter';
import { useService, Workspace } from '@toeverything/infra';
import { useService, WorkspaceService } from '@toeverything/infra';
import { useEffect, useState } from 'react';
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
@@ -17,12 +17,12 @@ import { FilterContainer } from './all-page-filter';
import { AllPageHeader } from './all-page-header';
export const AllPage = () => {
const currentWorkspace = useService(Workspace);
const currentWorkspace = useService(WorkspaceService).workspace;
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
const [filters, setFilters] = useState<Filter[]>([]);
const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
filters: filters,
});
@@ -59,7 +59,7 @@ export const AllPage = () => {
export const Component = () => {
performanceRenderLogger.info('AllPage');
const currentWorkspace = useService(Workspace);
const currentWorkspace = useService(WorkspaceService).workspace;
const navigateHelper = useNavigateHelper();
useEffect(() => {
@@ -31,12 +31,12 @@ const EmptyTagListHeader = () => {
};
export const AllTag = () => {
const tagService = useService(TagService);
const tags = useLiveData(tagService.tags$);
const tagList = useService(TagService).tagList;
const tags = useLiveData(tagList.tags$);
const [open, setOpen] = useState(false);
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
const tagMetas: TagMeta[] = useLiveData(tagService.tagMetas$);
const tagMetas: TagMeta[] = useLiveData(tagList.tagMetas$);
const handleCloseModal = useCallback(
(open: boolean) => {
@@ -16,7 +16,7 @@ import {
PageIcon,
ViewLayersIcon,
} from '@blocksuite/icons';
import { useLiveData, useService, Workspace } from '@toeverything/infra';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -67,7 +67,7 @@ export const Component = function CollectionPage() {
const collections = useLiveData(collectionService.collections$);
const navigate = useNavigateHelper();
const params = useParams();
const workspace = useService(Workspace);
const workspace = useService(WorkspaceService).workspace;
const collection = collections.find(v => v.id === params.collectionId);
const notifyCollectionDeleted = useCallback(() => {
@@ -103,7 +103,7 @@ export const Component = function CollectionPage() {
};
const Placeholder = ({ collection }: { collection: Collection }) => {
const workspace = useService(Workspace);
const workspace = useService(WorkspaceService).workspace;
const collectionService = useService(CollectionService);
const { node, open } = useEditCollection(useAllPageListConfig());
const { jumpToCollections } = useNavigateHelper();
@@ -1,7 +1,6 @@
import { Scrollable } from '@affine/component';
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import type { PageRootService } from '@blocksuite/blocks';
import {
BookmarkService,
@@ -14,29 +13,23 @@ import {
import { DisposableGroup } from '@blocksuite/global/utils';
import { type AffineEditorContainer, AIProvider } from '@blocksuite/presets';
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
import type { Doc } from '@toeverything/infra';
import {
Doc,
DocService,
DocsService,
FrameworkScope,
globalBlockSuiteSchema,
GlobalState,
GlobalContextService,
GlobalStateService,
LiveData,
PageManager,
PageRecordList,
ServiceProviderContext,
useLiveData,
useService,
Workspace,
WorkspaceService,
} from '@toeverything/infra';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import type { ReactElement } from 'react';
import {
memo,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useState,
} from 'react';
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import type { Map as YMap } from 'yjs';
@@ -58,7 +51,7 @@ import {
sidebarTabs,
} from '../../../modules/multi-tab-sidebar';
import {
RightSidebar,
RightSidebarService,
RightSidebarViewIsland,
} from '../../../modules/right-sidebar';
import {
@@ -74,8 +67,7 @@ import { DetailPageHeader } from './detail-page-header';
const RIGHT_SIDEBAR_TABS_ACTIVE_KEY = 'app:settings:rightsidebar:tabs:active';
const DetailPageImpl = memo(function DetailPageImpl() {
const globalState = useService(GlobalState);
const rightSidebar = useService(RightSidebar);
const globalState = useService(GlobalStateService).globalState;
const activeTabName = useLiveData(
LiveData.from(
globalState.watch<SidebarTabName>(RIGHT_SIDEBAR_TABS_ACTIVE_KEY),
@@ -89,13 +81,15 @@ const DetailPageImpl = memo(function DetailPageImpl() {
[globalState]
);
const page = useService(Doc);
const pageRecordList = useService(PageRecordList);
const currentPageId = page.id;
const doc = useService(DocService).doc;
const docRecordList = useService(DocsService).list;
const { openPage, jumpToTag } = useNavigateHelper();
const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
const currentWorkspace = useService(Workspace);
const docCollection = currentWorkspace.docCollection;
const workspace = useService(WorkspaceService).workspace;
const globalContext = useService(GlobalContextService).globalContext;
const rightSidebar = useService(RightSidebarService).rightSidebar;
const docCollection = workspace.docCollection;
const mode = useLiveData(doc.mode$);
const isActiveView = useIsActiveView();
// TODO: remove jotai here
@@ -116,15 +110,32 @@ const DetailPageImpl = memo(function DetailPageImpl() {
});
}, [activeTabName, rightSidebar, setActiveTabName]);
const pageMeta = useBlockSuiteDocMeta(docCollection).find(
meta => meta.id === page.id
);
useEffect(() => {
if (isActiveView) {
globalContext.docId.set(doc.id);
const isInTrash = pageMeta?.trash;
return () => {
globalContext.docId.set(null);
};
}
return;
}, [doc, globalContext, isActiveView]);
useEffect(() => {
if (isActiveView) {
globalContext.docMode.set(mode);
return () => {
globalContext.docId.set(null);
};
}
return;
}, [doc, globalContext, isActiveView, mode]);
const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
const mode = useLiveData(page.mode$);
useRegisterBlocksuiteEditorCommands();
const title = useLiveData(page.title$);
const title = useLiveData(doc.title$);
usePageDocumentTitle(title);
const onLoad = useCallback(
@@ -170,9 +181,9 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const disposable = new DisposableGroup();
pageService.getEditorMode = (pageId: string) =>
pageRecordList.record$(pageId).value?.mode$.value ?? 'page';
docRecordList.doc$(pageId).value?.mode$.value ?? 'page';
pageService.getDocUpdatedAt = (pageId: string) => {
const linkedPage = pageRecordList.record$(pageId).value;
const linkedPage = docRecordList.doc$(pageId).value;
if (!linkedPage) return new Date();
const updatedDate = linkedPage.meta$.value.updatedDate;
@@ -180,7 +191,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
return new Date(updatedDate || createDate || Date.now());
};
page.setMode(mode);
doc.setMode(mode);
// fixme: it seems pageLinkClicked is not triggered sometimes?
disposable.add(
pageService.slots.docLinkClicked.on(({ docId }) => {
@@ -189,12 +200,12 @@ const DetailPageImpl = memo(function DetailPageImpl() {
);
disposable.add(
pageService.slots.tagClicked.on(({ tagId }) => {
jumpToTag(currentWorkspace.id, tagId);
jumpToTag(workspace.id, tagId);
})
);
disposable.add(
pageService.slots.editorModeSwitch.on(mode => {
page.setMode(mode);
doc.setMode(mode);
})
);
@@ -205,13 +216,13 @@ const DetailPageImpl = memo(function DetailPageImpl() {
};
},
[
docCollection.id,
currentWorkspace.id,
jumpToTag,
doc,
mode,
docRecordList,
openPage,
page,
pageRecordList,
docCollection.id,
jumpToTag,
workspace.id,
]
);
@@ -220,16 +231,13 @@ const DetailPageImpl = memo(function DetailPageImpl() {
return (
<>
<ViewHeaderIsland>
<DetailPageHeader
page={page.blockSuiteDoc}
workspace={currentWorkspace}
/>
<DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} />
</ViewHeaderIsland>
<ViewBodyIsland>
<div className={styles.mainContainer}>
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
<AffineErrorBoundary key={currentPageId}>
<TopTip pageId={currentPageId} workspace={currentWorkspace} />
<AffineErrorBoundary key={doc.id}>
<TopTip pageId={doc.id} workspace={workspace} />
<Scrollable.Root>
<Scrollable.Viewport
className={clsx(
@@ -239,7 +247,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
)}
>
<PageDetailEditor
pageId={currentPageId}
pageId={doc.id}
onLoad={onLoad}
docCollection={docCollection}
/>
@@ -247,7 +255,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
<Scrollable.Scrollbar />
</Scrollable.Root>
</AffineErrorBoundary>
{isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
{isInTrash ? <TrashPageFooter /> : null}
</div>
</ViewBodyIsland>
@@ -282,7 +290,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
}
/>
<ImagePreviewModal pageId={currentPageId} docCollection={docCollection} />
<ImagePreviewModal pageId={doc.id} docCollection={docCollection} />
<GlobalPageHistoryModal />
<PageAIOnboarding />
</>
@@ -290,73 +298,65 @@ const DetailPageImpl = memo(function DetailPageImpl() {
});
export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
const currentWorkspace = useService(Workspace);
const pageRecordList = useService(PageRecordList);
const pageListReady = useLiveData(pageRecordList.isReady$);
const currentWorkspace = useService(WorkspaceService).workspace;
const docsService = useService(DocsService);
const docRecordList = docsService.list;
const docListReady = useLiveData(docRecordList.isReady$);
const docRecord = docRecordList.doc$(pageId).value;
const pageRecords = useLiveData(pageRecordList.records$);
const pageRecord = useMemo(
() => pageRecords.find(page => page.id === pageId),
[pageRecords, pageId]
);
const pageManager = useService(PageManager);
const [page, setPage] = useState<Doc | null>(null);
const [doc, setDoc] = useState<Doc | null>(null);
useLayoutEffect(() => {
if (!pageRecord) {
if (!docRecord) {
return;
}
const { page, release } = pageManager.open(pageRecord.id);
setPage(page);
const { doc: opened, release } = docsService.open(pageId);
setDoc(opened);
return () => {
release();
};
}, [pageManager, pageRecord]);
}, [docRecord, docsService, pageId]);
// set sync engine priority target
useEffect(() => {
currentWorkspace.setPriorityLoad(pageId, 10);
currentWorkspace.engine.doc.setPriority(pageId, 10);
return () => {
currentWorkspace.setPriorityLoad(pageId, 5);
currentWorkspace.engine.doc.setPriority(pageId, 5);
};
}, [currentWorkspace, pageId]);
const jumpOnce = useLiveData(pageRecord?.meta$.map(meta => meta.jumpOnce));
const jumpOnce = useLiveData(doc?.meta$.map(meta => meta.jumpOnce));
useEffect(() => {
if (jumpOnce) {
pageRecord?.setMeta({ jumpOnce: false });
doc?.record.setMeta({ jumpOnce: false });
}
}, [jumpOnce, pageRecord]);
}, [doc?.record, jumpOnce]);
const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash));
useEffect(() => {
if (page && pageRecord?.meta?.trash) {
if (doc && isInTrash) {
currentWorkspace.docCollection.awarenessStore.setReadonly(
page.blockSuiteDoc.blockCollection,
doc.blockSuiteDoc.blockCollection,
true
);
}
}, [
currentWorkspace.docCollection.awarenessStore,
page,
pageRecord?.meta?.trash,
]);
}, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]);
// if sync engine has been synced and the page is null, show 404 page.
if (pageListReady && !page) {
if (docListReady && !doc) {
return <PageNotFound noPermission />;
}
if (!page) {
if (!doc) {
return <PageDetailSkeleton key="current-page-is-null" />;
}
return (
<ServiceProviderContext.Provider value={page.services}>
<FrameworkScope scope={doc.scope}>
<DetailPageImpl />
</ServiceProviderContext.Provider>
</FrameworkScope>
);
};
@@ -2,14 +2,14 @@ import { useWorkspace } from '@affine/core/hooks/use-workspace';
import { ZipTransformer } from '@blocksuite/blocks';
import type { Workspace } from '@toeverything/infra';
import {
ServiceProviderContext,
FrameworkScope,
GlobalContextService,
useLiveData,
useService,
WorkspaceListService,
WorkspaceManager,
WorkspacesService,
} from '@toeverything/infra';
import type { ReactElement } from 'react';
import { Suspense, useEffect, useMemo } from 'react';
import { Suspense, useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
@@ -17,7 +17,6 @@ import { WorkspaceFallback } from '../../components/workspace';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
import { RightSidebarContainer } from '../../modules/right-sidebar';
import { WorkbenchRoot } from '../../modules/workbench';
import { CurrentWorkspaceService } from '../../modules/workspace/current-workspace';
import { AllWorkspaceModals } from '../../providers/modal-provider';
import { performanceRenderLogger } from '../../shared';
import { PageNotFound } from '../404';
@@ -38,62 +37,87 @@ declare global {
export const Component = (): ReactElement => {
performanceRenderLogger.info('WorkspaceLayout');
const currentWorkspaceService = useService(CurrentWorkspaceService);
const params = useParams();
const { workspaceList, loading: listLoading } = useLiveData(
useService(WorkspaceListService).status$
);
const workspaceManager = useService(WorkspaceManager);
const [showNotFound, setShowNotFound] = useState(false);
const workspacesService = useService(WorkspacesService);
const listLoading = useLiveData(workspacesService.list.isLoading$);
const workspaces = useLiveData(workspacesService.list.workspaces$);
const meta = useMemo(() => {
return workspaceList.find(({ id }) => id === params.workspaceId);
}, [workspaceList, params.workspaceId]);
return workspaces.find(({ id }) => id === params.workspaceId);
}, [workspaces, params.workspaceId]);
const workspace = useWorkspace(meta);
const globalContext = useService(GlobalContextService).globalContext;
useEffect(() => {
if (!workspace) {
currentWorkspaceService.closeWorkspace();
return undefined;
}
currentWorkspaceService.openWorkspace(workspace ?? null);
workspacesService.list.revalidate();
}, [workspacesService]);
// for debug purpose
window.currentWorkspace = workspace;
window.exportWorkspaceSnapshot = async () => {
const zip = await ZipTransformer.exportDocs(
workspace.docCollection,
Array.from(workspace.docCollection.docs.values()).map(collection =>
collection.getDoc()
)
useEffect(() => {
if (workspace) {
// for debug purpose
window.currentWorkspace = workspace ?? undefined;
window.dispatchEvent(
new CustomEvent('affine:workspace:change', {
detail: {
id: workspace.id,
},
})
);
const url = URL.createObjectURL(zip);
// download url
const a = document.createElement('a');
a.href = url;
a.download = `${workspace.docCollection.meta.name}.zip`;
a.click();
URL.revokeObjectURL(url);
};
window.dispatchEvent(
new CustomEvent('affine:workspace:change', {
detail: {
id: workspace.id,
},
})
);
localStorage.setItem('last_workspace_id', workspace.id);
}, [meta, workspaceManager, workspace, currentWorkspaceService]);
window.exportWorkspaceSnapshot = async () => {
const zip = await ZipTransformer.exportDocs(
workspace.docCollection,
Array.from(workspace.docCollection.docs.values()).map(collection =>
collection.getDoc()
)
);
const url = URL.createObjectURL(zip);
// download url
const a = document.createElement('a');
a.href = url;
a.download = `${workspace.docCollection.meta.name}.zip`;
a.click();
URL.revokeObjectURL(url);
};
localStorage.setItem('last_workspace_id', workspace.id);
globalContext.workspaceId.set(workspace.id);
return () => {
window.currentWorkspace = undefined;
globalContext.workspaceId.set(null);
};
}
return;
}, [globalContext, meta, workspace]);
// avoid doing operation, before workspace is loaded
const isRootDocReady =
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
// if listLoading is false, we can show 404 page, otherwise we should show loading page.
if (listLoading === false && meta === undefined) {
useEffect(() => {
if (listLoading === false && meta === undefined) {
setShowNotFound(true);
}
if (meta) {
setShowNotFound(false);
}
}, [listLoading, meta, workspacesService]);
useEffect(() => {
if (showNotFound) {
const timer = setInterval(() => {
workspacesService.list.revalidate();
}, 3000);
return () => {
clearInterval(timer);
};
}
return;
}, [showNotFound, workspacesService]);
if (showNotFound) {
return <PageNotFound noPermission />;
}
if (!workspace) {
@@ -102,15 +126,15 @@ export const Component = (): ReactElement => {
if (!isRootDocReady) {
return (
<ServiceProviderContext.Provider value={workspace.services}>
<FrameworkScope scope={workspace.scope}>
<WorkspaceFallback key="workspaceLoading" />
<AllWorkspaceModals />
</ServiceProviderContext.Provider>
</FrameworkScope>
);
}
return (
<ServiceProviderContext.Provider value={workspace.services}>
<FrameworkScope scope={workspace.scope}>
<Suspense fallback={<WorkspaceFallback key="workspaceFallback" />}>
<AffineErrorBoundary height="100vh">
<WorkspaceLayout>
@@ -119,6 +143,6 @@ export const Component = (): ReactElement => {
</WorkspaceLayout>
</AffineErrorBoundary>
</Suspense>
</ServiceProviderContext.Provider>
</FrameworkScope>
);
};
@@ -8,7 +8,7 @@ import {
ViewBodyIsland,
ViewHeaderIsland,
} from '@affine/core/modules/workbench';
import { useLiveData, useService, Workspace } from '@toeverything/infra';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
@@ -18,11 +18,11 @@ import { TagDetailHeader } from './header';
import * as styles from './index.css';
export const TagDetail = ({ tagId }: { tagId?: string }) => {
const currentWorkspace = useService(Workspace);
const currentWorkspace = useService(WorkspaceService).workspace;
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
const tagService = useService(TagService);
const currentTag = useLiveData(tagService.tagByTagId$(tagId));
const tagList = useService(TagService).tagList;
const currentTag = useLiveData(tagList.tagByTagId$(tagId));
const pageIds = useLiveData(currentTag?.pageIds$);
@@ -16,7 +16,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { DeleteIcon } from '@blocksuite/icons';
import type { DocMeta } from '@blocksuite/store';
import { useService, Workspace } from '@toeverything/infra';
import { useService, WorkspaceService } from '@toeverything/infra';
import { useCallback } from 'react';
import { ViewBodyIsland, ViewHeaderIsland } from '../../modules/workbench';
@@ -38,12 +38,12 @@ const TrashHeader = () => {
};
export const TrashPage = () => {
const currentWorkspace = useService(Workspace);
const currentWorkspace = useService(WorkspaceService).workspace;
const docCollection = currentWorkspace.docCollection;
assertExists(docCollection);
const pageMetas = useBlockSuiteDocMeta(docCollection);
const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
trash: true,
});