feat!: affine cloud support (#3813)

Co-authored-by: Hongtao Lye <codert.sn@gmail.com>
Co-authored-by: liuyi <forehalo@gmail.com>
Co-authored-by: LongYinan <lynweklm@gmail.com>
Co-authored-by: X1a0t <405028157@qq.com>
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
Co-authored-by: Qi <474021214@qq.com>
Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
Alex Yang
2023-08-29 05:07:05 -05:00
committed by GitHub
parent d0145c6f38
commit 2f6c4e3696
414 changed files with 19469 additions and 7591 deletions
+133
View File
@@ -0,0 +1,133 @@
import {
ChangeEmailPage,
ChangePasswordPage,
SetPasswordPage,
SignInSuccessPage,
SignUpPage,
} from '@affine/component/auth-components';
import { isDesktop } from '@affine/env/constant';
import { changeEmailMutation, changePasswordMutation } from '@affine/graphql';
import { useMutation } from '@affine/workspace/affine/gql';
import type { ReactElement } from 'react';
import { useCallback } from 'react';
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
import { z } from 'zod';
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
import { useCurrentUser } from '../hooks/affine/use-current-user';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
const authTypeSchema = z.enum([
'setPassword',
'signIn',
'changePassword',
'signUp',
'changeEmail',
]);
export const AuthPage = (): ReactElement | null => {
const user = useCurrentUser();
const { authType } = useParams();
const { trigger: changePassword } = useMutation({
mutation: changePasswordMutation,
});
const { trigger: changeEmail } = useMutation({
mutation: changeEmailMutation,
});
const { jumpToIndex } = useNavigateHelper();
const onChangeEmail = useCallback(
async (email: string) => {
const res = await changeEmail({
id: user.id,
newEmail: email,
});
return !!res?.changeEmail;
},
[changeEmail, user.id]
);
const onSetPassword = useCallback(
(password: string) => {
changePassword({
id: user.id,
newPassword: password,
}).catch(console.error);
},
[changePassword, user.id]
);
const onOpenAffine = useCallback(() => {
if (isDesktop) {
window.apis.ui.handleFinishLogin();
} else {
jumpToIndex(RouteLogic.REPLACE);
}
}, [jumpToIndex]);
switch (authType) {
case 'signUp': {
return (
<SignUpPage
user={user}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
/>
);
}
case 'signIn': {
return <SignInSuccessPage onOpenAffine={onOpenAffine} />;
}
case 'changePassword': {
return (
<ChangePasswordPage
user={user}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
/>
);
}
case 'setPassword': {
return (
<SetPasswordPage
user={user}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
/>
);
}
case 'changeEmail': {
return (
<ChangeEmailPage
user={user}
onChangeEmail={onChangeEmail}
onOpenAffine={onOpenAffine}
/>
);
}
}
return null;
};
export const loader: LoaderFunction = async args => {
if (!args.params.authType) {
return redirect('/404');
}
if (!authTypeSchema.safeParse(args.params.authType).success) {
return redirect('/404');
}
return null;
};
export const Component = () => {
const loginStatus = useCurrenLoginStatus();
const { jumpToExpired } = useNavigateHelper();
if (loginStatus === 'unauthenticated') {
jumpToExpired(RouteLogic.REPLACE);
}
if (loginStatus === 'authenticated') {
return <AuthPage />;
}
return null;
};
+25
View File
@@ -0,0 +1,25 @@
import { AuthPageContainer } from '@affine/component/auth-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useCallback } from 'react';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
export const Component = () => {
const t = useAFFiNEI18N();
const { jumpToIndex } = useNavigateHelper();
const onOpenAffine = useCallback(() => {
jumpToIndex(RouteLogic.REPLACE);
}, [jumpToIndex]);
return (
<AuthPageContainer
title={t['com.affine.expired.page.title']()}
subtitle={t['com.affine.expired.page.subtitle']()}
>
<Button type="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
);
};
+103
View File
@@ -0,0 +1,103 @@
import { AcceptInvitePage } from '@affine/component/member-components';
import { WorkspaceSubPath } from '@affine/env/workspace';
import {
acceptInviteByInviteIdMutation,
type GetInviteInfoQuery,
getInviteInfoQuery,
} from '@affine/graphql';
import { fetcher } from '@affine/workspace/affine/gql';
import { useAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import { type LoaderFunction, redirect, useLoaderData } from 'react-router-dom';
import { authAtom } from '../atoms';
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { useAppHelper } from '../hooks/use-workspaces';
export const loader: LoaderFunction = async args => {
const inviteId = args.params.inviteId || '';
const res = await fetcher({
query: getInviteInfoQuery,
variables: {
inviteId,
},
}).catch(console.error);
// If the inviteId is invalid, redirect to 404 page
if (!res || !res?.getInviteInfo) {
return redirect('/404');
}
// No mater sign in or not, we need to accept the invite
await fetcher({
query: acceptInviteByInviteIdMutation,
variables: {
workspaceId: res.getInviteInfo.workspace.id,
inviteId,
},
}).catch(console.error);
return {
inviteId,
inviteInfo: res.getInviteInfo,
};
};
export const Component = () => {
const loginStatus = useCurrenLoginStatus();
const { jumpToSignIn } = useNavigateHelper();
const { addCloudWorkspace } = useAppHelper();
const { jumpToSubPath } = useNavigateHelper();
const [, setAuthAtom] = useAtom(authAtom);
const { inviteInfo } = useLoaderData() as {
inviteId: string;
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
};
const loadWorkspaceAfterSignIn = useCallback(() => {
addCloudWorkspace(inviteInfo.workspace.id);
}, [addCloudWorkspace, inviteInfo.workspace.id]);
const openWorkspace = useCallback(() => {
addCloudWorkspace(inviteInfo.workspace.id);
jumpToSubPath(
inviteInfo.workspace.id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
);
}, [addCloudWorkspace, inviteInfo.workspace.id, jumpToSubPath]);
useEffect(() => {
if (loginStatus === 'unauthenticated') {
// We can not pass function to navigate state, so we need to save it in atom
setAuthAtom(prev => ({
...prev,
onceSignedIn: loadWorkspaceAfterSignIn,
}));
jumpToSignIn(RouteLogic.REPLACE, {
state: {
callbackURL: `/workspace/${inviteInfo.workspace.id}/all`,
},
});
}
}, [
inviteInfo.workspace.id,
jumpToSignIn,
loadWorkspaceAfterSignIn,
loginStatus,
setAuthAtom,
]);
if (loginStatus === 'authenticated') {
return (
<AcceptInvitePage
inviteInfo={inviteInfo}
onOpenWorkspace={openWorkspace}
/>
);
}
return null;
};
+73
View File
@@ -0,0 +1,73 @@
import { MainContainer } from '@affine/component/workspace';
import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { downloadBinaryFromCloud } from '@affine/workspace/providers';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { noop } from 'foxact/noop';
import type { ReactElement } from 'react';
import { useCallback } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect, useLoaderData } from 'react-router-dom';
import { applyUpdate } from 'yjs';
import { PageDetailEditor } from '../../adapters/shared';
import { AppContainer } from '../../components/affine/app-container';
function assertArrayBuffer(value: unknown): asserts value is ArrayBuffer {
if (!(value instanceof ArrayBuffer)) {
throw new Error('value is not ArrayBuffer');
}
}
const logger = new DebugLogger('public:share-page');
export const loader: LoaderFunction = async ({ params }) => {
const workspaceId = params?.workspaceId;
const pageId = params?.pageId;
if (!workspaceId || !pageId) {
return redirect('/404');
}
const workspace = getOrCreateWorkspace(
workspaceId,
WorkspaceFlavour.AFFINE_PUBLIC
);
// download root workspace
{
const buffer = await downloadBinaryFromCloud(workspaceId, workspaceId);
assertArrayBuffer(buffer);
applyUpdate(workspace.doc, new Uint8Array(buffer));
}
const page = workspace.getPage(pageId);
assertExists(page, 'cannot find page');
// download page
{
const buffer = await downloadBinaryFromCloud(
workspaceId,
page.spaceDoc.guid
);
assertArrayBuffer(buffer);
applyUpdate(page.spaceDoc, new Uint8Array(buffer));
}
logger.info('workspace', workspace);
workspace.awarenessStore.setReadonly(page, true);
return page;
};
export const Component = (): ReactElement => {
const page = useLoaderData() as Page;
return (
<AppContainer>
<MainContainer>
<PageDetailEditor
isPublic
workspace={page.workspace}
pageId={page.id}
onInit={useCallback(() => noop, [])}
onLoad={useCallback(() => noop, [])}
/>
</MainContainer>
</AppContainer>
);
};
+75
View File
@@ -0,0 +1,75 @@
import { SignInPageContainer } from '@affine/component/auth-components';
import { useAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useLocation, useNavigate } from 'react-router-dom';
import { authAtom } from '../atoms';
import { AuthPanel } from '../components/affine/auth';
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
interface LocationState {
state: {
callbackURL?: string;
};
}
export const Component = () => {
const [
{ state, email = '', emailType = 'changePassword', onceSignedIn },
setAuthAtom,
] = useAtom(authAtom);
const loginStatus = useCurrenLoginStatus();
const location = useLocation() as LocationState;
const navigate = useNavigate();
useEffect(() => {
const afterSignedIn = async () => {
if (loginStatus === 'authenticated') {
if (onceSignedIn) {
await onceSignedIn();
setAuthAtom(prev => ({ ...prev, onceSignedIn: undefined }));
}
if (location.state.callbackURL) {
navigate(location.state.callbackURL, {
replace: true,
});
}
}
};
afterSignedIn();
}, [
location.state.callbackURL,
loginStatus,
navigate,
onceSignedIn,
setAuthAtom,
]);
return (
<SignInPageContainer>
<AuthPanel
state={state}
email={email}
emailType={emailType}
setEmailType={useCallback(
emailType => {
setAuthAtom(prev => ({ ...prev, emailType }));
},
[setAuthAtom]
)}
setAuthState={useCallback(
state => {
setAuthAtom(prev => ({ ...prev, state }));
},
[setAuthAtom]
)}
setAuthEmail={useCallback(
email => {
setAuthAtom(prev => ({ ...prev, email }));
},
[setAuthAtom]
)}
/>
</SignInPageContainer>
);
};