mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
feat(core): desktop multiple server support (#8979)
This commit is contained in:
@@ -1,121 +0,0 @@
|
||||
import { notify } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
BackButton,
|
||||
CountDownRender,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { CaptchaService } from '@affine/core/modules/cloud';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
import { Captcha } from './use-captcha';
|
||||
|
||||
export const AfterSignUpSendEmail: FC<
|
||||
AuthPanelProps<'afterSignUpSendEmail'>
|
||||
> = ({ setAuthData, email, redirectUrl }) => {
|
||||
const [resendCountDown, setResendCountDown] = useState(60);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setResendCountDown(c => Math.max(c - 1, 0));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const captchaService = useService(CaptchaService);
|
||||
|
||||
const verifyToken = useLiveData(captchaService.verifyToken$);
|
||||
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
||||
const challenge = useLiveData(captchaService.challenge$);
|
||||
|
||||
const onResendClick = useAsyncCallback(async () => {
|
||||
setIsSending(true);
|
||||
try {
|
||||
captchaService.revalidate();
|
||||
await authService.sendEmailMagicLink(
|
||||
email,
|
||||
verifyToken,
|
||||
challenge,
|
||||
redirectUrl
|
||||
);
|
||||
setResendCountDown(60);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
title: 'Failed to send email, please try again.',
|
||||
});
|
||||
}
|
||||
setIsSending(false);
|
||||
}, [authService, captchaService, challenge, email, redirectUrl, verifyToken]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.up']()}
|
||||
subTitle={t['com.affine.auth.sign.up.sent.email.subtitle']()}
|
||||
/>
|
||||
<AuthContent style={{ height: 100 }}>
|
||||
<Trans
|
||||
i18nKey="com.affine.auth.sign.sent.email.message.sent-tips"
|
||||
values={{ email }}
|
||||
components={{ a: <a href={`mailto:${email}`} /> }}
|
||||
/>
|
||||
{t['com.affine.auth.sign.sent.email.message.sent-tips.sign-up']()}
|
||||
</AuthContent>
|
||||
|
||||
<div className={style.resendWrapper}>
|
||||
{resendCountDown <= 0 ? (
|
||||
<>
|
||||
<Captcha />
|
||||
<Button
|
||||
style={
|
||||
!verifyToken && needCaptcha ? { cursor: 'not-allowed' } : {}
|
||||
}
|
||||
disabled={(!verifyToken && needCaptcha) || isSending}
|
||||
variant="plain"
|
||||
size="large"
|
||||
onClick={onResendClick}
|
||||
>
|
||||
{t['com.affine.auth.sign.auth.code.resend.hint']()}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={style.sentRow}>
|
||||
<div className={style.sentMessage}>
|
||||
{t['com.affine.auth.sent']()}
|
||||
</div>
|
||||
<CountDownRender
|
||||
className={style.resendCountdown}
|
||||
timeLeft={resendCountDown}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||
{t['com.affine.auth.sign.auth.code.message']()}
|
||||
</div>
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthData({ state: 'signIn' });
|
||||
}, [setAuthData])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useConfirmModal } from '@affine/component';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
export const showAILoginRequiredAtom = atom(false);
|
||||
@@ -9,12 +10,12 @@ export const showAILoginRequiredAtom = atom(false);
|
||||
export const AiLoginRequiredModal = () => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useAtom(showAILoginRequiredAtom);
|
||||
const setAuth = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
|
||||
|
||||
const openSignIn = useCallback(() => {
|
||||
setAuth(prev => ({ ...prev, openModal: true }));
|
||||
}, [setAuth]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { AuthModal as AuthModalBase } from '@affine/component/auth-components';
|
||||
import { authAtom, type AuthAtomData } from '@affine/core/components/atoms';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai/react';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
||||
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
||||
import { SendEmail } from './send-email';
|
||||
import { SignIn } from './sign-in';
|
||||
import { SignInWithPassword } from './sign-in-with-password';
|
||||
|
||||
type AuthAtomType<T extends AuthAtomData['state']> = Extract<
|
||||
AuthAtomData,
|
||||
{ state: T }
|
||||
>;
|
||||
|
||||
// return field in B that is not in A
|
||||
type Difference<
|
||||
A extends Record<string, any>,
|
||||
B extends Record<string, any>,
|
||||
> = Pick<B, Exclude<keyof B, keyof A>>;
|
||||
|
||||
export type AuthPanelProps<State extends AuthAtomData['state']> = {
|
||||
setAuthData: <T extends AuthAtomData['state']>(
|
||||
updates: { state: T } & Difference<AuthAtomType<State>, AuthAtomType<T>>
|
||||
) => void;
|
||||
onSkip?: () => void;
|
||||
redirectUrl?: string;
|
||||
} & Extract<AuthAtomData, { state: State }>;
|
||||
|
||||
const config: {
|
||||
[k in AuthAtomData['state']]: FC<AuthPanelProps<k>>;
|
||||
} = {
|
||||
signIn: SignIn,
|
||||
afterSignUpSendEmail: AfterSignUpSendEmail,
|
||||
afterSignInSendEmail: AfterSignInSendEmail,
|
||||
signInWithPassword: SignInWithPassword,
|
||||
sendEmail: SendEmail,
|
||||
};
|
||||
|
||||
export function AuthModal() {
|
||||
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
||||
const setOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
setAuthAtom(prev => ({ ...prev, openModal: open }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
);
|
||||
|
||||
return (
|
||||
<AuthModalBase open={authAtomValue.openModal} setOpen={setOpen}>
|
||||
<AuthPanel />
|
||||
</AuthModalBase>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthPanel({
|
||||
onSkip,
|
||||
redirectUrl,
|
||||
}: {
|
||||
onSkip?: () => void;
|
||||
redirectUrl?: string | null;
|
||||
}) {
|
||||
const t = useI18n();
|
||||
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
||||
const authService = useService(AuthService);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
const previousLoginStatus = useRef(loginStatus);
|
||||
|
||||
const setAuthData = useCallback(
|
||||
(updates: Partial<AuthAtomData>) => {
|
||||
// @ts-expect-error checked in impls
|
||||
setAuthAtom(prev => ({
|
||||
...prev,
|
||||
...updates,
|
||||
}));
|
||||
},
|
||||
[setAuthAtom]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
loginStatus === 'authenticated' &&
|
||||
previousLoginStatus.current === 'unauthenticated'
|
||||
) {
|
||||
setAuthAtom({
|
||||
openModal: false,
|
||||
state: 'signIn',
|
||||
});
|
||||
notify.success({
|
||||
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||
});
|
||||
}
|
||||
previousLoginStatus.current = loginStatus;
|
||||
}, [loginStatus, setAuthAtom, t]);
|
||||
|
||||
const CurrentPanel = config[authAtomValue.state];
|
||||
|
||||
const props = {
|
||||
...authAtomValue,
|
||||
onSkip,
|
||||
redirectUrl,
|
||||
setAuthData,
|
||||
};
|
||||
|
||||
// @ts-expect-error checked in impls
|
||||
return <CurrentPanel {...props} />;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Skeleton } from '@affine/component';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { ServerService } from '@affine/core/modules/cloud';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
@@ -38,7 +37,7 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
|
||||
const scheme = urlService.getClientScheme();
|
||||
|
||||
if (!oauth) {
|
||||
return <Skeleton height={50} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
return oauthProviders?.map(provider => (
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
import { notify, Wrapper } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
AuthInput,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import {
|
||||
sendChangeEmailMutation,
|
||||
sendChangePasswordEmailMutation,
|
||||
sendSetPasswordEmailMutation,
|
||||
sendVerifyEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useMutation } from '../../../components/hooks/use-mutation';
|
||||
import { ServerService } from '../../../modules/cloud';
|
||||
import type { AuthPanelProps } from './index';
|
||||
|
||||
const useEmailTitle = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
|
||||
const t = useI18n();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.set.password']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.reset.password']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.settings.email.action.change']();
|
||||
case 'verifyEmail':
|
||||
return t['com.affine.settings.email.action.verify']();
|
||||
}
|
||||
};
|
||||
|
||||
const useNotificationHint = (
|
||||
emailType: AuthPanelProps<'sendEmail'>['emailType']
|
||||
) => {
|
||||
const t = useI18n();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.sent.set.password.hint']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.sent.change.password.hint']();
|
||||
case 'changeEmail':
|
||||
case 'verifyEmail':
|
||||
return t['com.affine.auth.sent.verify.email.hint']();
|
||||
}
|
||||
};
|
||||
const useButtonContent = (
|
||||
emailType: AuthPanelProps<'sendEmail'>['emailType']
|
||||
) => {
|
||||
const t = useI18n();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.send.set.password.link']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.send.reset.password.link']();
|
||||
case 'changeEmail':
|
||||
case 'verifyEmail':
|
||||
return t['com.affine.auth.send.verify.email.hint']();
|
||||
}
|
||||
};
|
||||
|
||||
const useSendEmail = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
|
||||
const {
|
||||
trigger: sendChangePasswordEmail,
|
||||
isMutating: isChangePasswordMutating,
|
||||
} = useMutation({
|
||||
mutation: sendChangePasswordEmailMutation,
|
||||
});
|
||||
const { trigger: sendSetPasswordEmail, isMutating: isSetPasswordMutating } =
|
||||
useMutation({
|
||||
mutation: sendSetPasswordEmailMutation,
|
||||
});
|
||||
const { trigger: sendChangeEmail, isMutating: isChangeEmailMutating } =
|
||||
useMutation({
|
||||
mutation: sendChangeEmailMutation,
|
||||
});
|
||||
const { trigger: sendVerifyEmail, isMutating: isVerifyEmailMutation } =
|
||||
useMutation({
|
||||
mutation: sendVerifyEmailMutation,
|
||||
});
|
||||
|
||||
return {
|
||||
loading:
|
||||
isChangePasswordMutating ||
|
||||
isSetPasswordMutating ||
|
||||
isChangeEmailMutating ||
|
||||
isVerifyEmailMutation,
|
||||
sendEmail: useCallback(
|
||||
(email: string) => {
|
||||
let trigger: (args: {
|
||||
email: string;
|
||||
callbackUrl: string;
|
||||
}) => Promise<unknown>;
|
||||
let callbackUrl;
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
trigger = sendSetPasswordEmail;
|
||||
callbackUrl = 'setPassword';
|
||||
break;
|
||||
case 'changePassword':
|
||||
trigger = sendChangePasswordEmail;
|
||||
callbackUrl = 'changePassword';
|
||||
break;
|
||||
case 'changeEmail':
|
||||
trigger = sendChangeEmail;
|
||||
callbackUrl = 'changeEmail';
|
||||
break;
|
||||
case 'verifyEmail':
|
||||
trigger = sendVerifyEmail;
|
||||
callbackUrl = 'verify-email';
|
||||
break;
|
||||
}
|
||||
// TODO(@eyhn): add error handler
|
||||
return trigger({
|
||||
email,
|
||||
callbackUrl: `/auth/${callbackUrl}`,
|
||||
});
|
||||
},
|
||||
[
|
||||
emailType,
|
||||
sendChangeEmail,
|
||||
sendChangePasswordEmail,
|
||||
sendSetPasswordEmail,
|
||||
sendVerifyEmail,
|
||||
]
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const SendEmail = ({
|
||||
setAuthData,
|
||||
email,
|
||||
emailType,
|
||||
// todo(@pengx17): impl redirectUrl for sendEmail?
|
||||
}: AuthPanelProps<'sendEmail'>) => {
|
||||
const t = useI18n();
|
||||
const serverService = useService(ServerService);
|
||||
|
||||
const passwordLimits = useLiveData(
|
||||
serverService.server.credentialsRequirement$.map(r => r?.password)
|
||||
);
|
||||
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||
|
||||
const title = useEmailTitle(emailType);
|
||||
const hint = useNotificationHint(emailType);
|
||||
const buttonContent = useButtonContent(emailType);
|
||||
const { loading, sendEmail } = useSendEmail(emailType);
|
||||
|
||||
const onSendEmail = useAsyncCallback(async () => {
|
||||
// TODO(@eyhn): add error handler
|
||||
await sendEmail(email);
|
||||
|
||||
notify.success({ title: hint });
|
||||
setHasSentEmail(true);
|
||||
}, [email, hint, sendEmail]);
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
setAuthData({ state: 'signIn' });
|
||||
}, [setAuthData]);
|
||||
|
||||
if (!passwordLimits) {
|
||||
// TODO(@eyhn): loading & error UI
|
||||
return null;
|
||||
}
|
||||
|
||||
const content =
|
||||
emailType === 'setPassword'
|
||||
? t['com.affine.auth.set.password.message']({
|
||||
min: String(passwordLimits.minLength),
|
||||
max: String(passwordLimits.maxLength),
|
||||
})
|
||||
: emailType === 'changePassword'
|
||||
? t['com.affine.auth.reset.password.message']()
|
||||
: emailType === 'changeEmail' || emailType === 'verifyEmail'
|
||||
? t['com.affine.auth.verify.email.message']({ email })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.brand.affineCloud']()}
|
||||
subTitle={title}
|
||||
/>
|
||||
<AuthContent>{content}</AuthContent>
|
||||
|
||||
<Wrapper
|
||||
marginTop={30}
|
||||
marginBottom={50}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
disabled={true}
|
||||
value={email}
|
||||
/>
|
||||
</Wrapper>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="extraLarge"
|
||||
style={{ width: '100%' }}
|
||||
disabled={hasSentEmail}
|
||||
loading={loading}
|
||||
onClick={onSendEmail}
|
||||
>
|
||||
{hasSentEmail ? t['com.affine.auth.sent']() : buttonContent}
|
||||
</Button>
|
||||
<BackButton onClick={onBack} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,184 +0,0 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { CaptchaService } from '@affine/core/modules/cloud';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { ArrowRightBigIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import type { FC } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { emailRegex } from '../../../utils/email-regex';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import { OAuth } from './oauth';
|
||||
import * as style from './style.css';
|
||||
import { Captcha } from './use-captcha';
|
||||
|
||||
function validateEmail(email: string) {
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
export const SignIn: FC<AuthPanelProps<'signIn'>> = ({
|
||||
setAuthData: setAuthState,
|
||||
onSkip,
|
||||
redirectUrl,
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const captchaService = useService(CaptchaService);
|
||||
|
||||
const verifyToken = useLiveData(captchaService.verifyToken$);
|
||||
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
||||
const challenge = useLiveData(captchaService.challenge$);
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||
const errorMsg = searchParams.get('error');
|
||||
|
||||
const onContinue = useAsyncCallback(async () => {
|
||||
if (!validateEmail(email)) {
|
||||
setIsValidEmail(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidEmail(true);
|
||||
setIsMutating(true);
|
||||
|
||||
try {
|
||||
const { hasPassword, registered } =
|
||||
await authService.checkUserByEmail(email);
|
||||
|
||||
if (registered) {
|
||||
// provider password sign-in if user has by default
|
||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||
if (hasPassword) {
|
||||
setAuthState({
|
||||
state: 'signInWithPassword',
|
||||
email,
|
||||
});
|
||||
} else {
|
||||
captchaService.revalidate();
|
||||
await authService.sendEmailMagicLink(
|
||||
email,
|
||||
verifyToken,
|
||||
challenge,
|
||||
redirectUrl
|
||||
);
|
||||
setAuthState({
|
||||
state: 'afterSignInSendEmail',
|
||||
email,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
captchaService.revalidate();
|
||||
await authService.sendEmailMagicLink(
|
||||
email,
|
||||
verifyToken,
|
||||
challenge,
|
||||
redirectUrl
|
||||
);
|
||||
setAuthState({
|
||||
state: 'afterSignUpSendEmail',
|
||||
email,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
// TODO(@eyhn): better error handling
|
||||
notify.error({
|
||||
title: 'Failed to send email. Please try again.',
|
||||
});
|
||||
}
|
||||
|
||||
setIsMutating(false);
|
||||
}, [
|
||||
authService,
|
||||
captchaService,
|
||||
challenge,
|
||||
email,
|
||||
redirectUrl,
|
||||
setAuthState,
|
||||
verifyToken,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={t['com.affine.brand.affineCloud']()}
|
||||
/>
|
||||
|
||||
<OAuth redirectUrl={redirectUrl} />
|
||||
|
||||
<div className={style.authModalContent}>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
||||
onChange={setEmail}
|
||||
error={!isValidEmail}
|
||||
errorHint={
|
||||
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
||||
}
|
||||
onEnter={onContinue}
|
||||
/>
|
||||
|
||||
{verifyToken || !needCaptcha ? (
|
||||
<Button
|
||||
style={{ width: '100%' }}
|
||||
size="extraLarge"
|
||||
data-testid="continue-login-button"
|
||||
block
|
||||
loading={isMutating}
|
||||
suffix={<ArrowRightBigIcon />}
|
||||
suffixStyle={{ width: 20, height: 20, color: cssVar('blue') }}
|
||||
onClick={onContinue}
|
||||
>
|
||||
{t['com.affine.auth.sign.email.continue']()}
|
||||
</Button>
|
||||
) : (
|
||||
<Captcha />
|
||||
)}
|
||||
|
||||
{errorMsg && <div className={style.errorMessage}>{errorMsg}</div>}
|
||||
|
||||
<div className={style.authMessage}>
|
||||
{/*prettier-ignore*/}
|
||||
<Trans i18nKey="com.affine.auth.sign.message">
|
||||
By clicking "Continue with Google/Email" above, you acknowledge that
|
||||
you agree to AFFiNE's <a href="https://affine.pro/terms" target="_blank" rel="noreferrer">Terms of Conditions</a> and <a href="https://affine.pro/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>.
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{onSkip ? (
|
||||
<>
|
||||
<div className={style.skipDivider}>
|
||||
<div className={style.skipDividerLine} />
|
||||
<span className={style.skipDividerText}>or</span>
|
||||
<div className={style.skipDividerLine} />
|
||||
</div>
|
||||
<div className={style.skipSection}>
|
||||
<div className={style.skipText}>
|
||||
{t['com.affine.mobile.sign-in.skip.hint']()}
|
||||
</div>
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={onSkip}
|
||||
className={style.skipLink}
|
||||
suffix={<ArrowRightBigIcon className={style.skipLinkIcon} />}
|
||||
>
|
||||
{t['com.affine.mobile.sign-in.skip.link']()}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,99 +1,6 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const authModalContent = style({
|
||||
marginTop: '30px',
|
||||
});
|
||||
export const captchaWrapper = style({
|
||||
margin: 'auto',
|
||||
marginBottom: '4px',
|
||||
textAlign: 'center',
|
||||
});
|
||||
export const authMessage = style({
|
||||
marginTop: '30px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: 1.5,
|
||||
});
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const errorMessage = style({
|
||||
marginTop: '30px',
|
||||
color: cssVar('textHighlightForegroundRed'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: 1.5,
|
||||
});
|
||||
|
||||
globalStyle(`${authMessage} a`, {
|
||||
color: cssVar('linkColor'),
|
||||
});
|
||||
globalStyle(`${authMessage} .link`, {
|
||||
cursor: 'pointer',
|
||||
color: cssVar('linkColor'),
|
||||
});
|
||||
export const forgetPasswordButtonRow = style({
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
marginTop: '-26px', // Let this button be a tail of password input.
|
||||
});
|
||||
export const sendMagicLinkButtonRow = style({
|
||||
marginBottom: '30px',
|
||||
});
|
||||
export const linkButton = style({
|
||||
color: cssVar('linkColor'),
|
||||
background: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '22px',
|
||||
userSelect: 'none',
|
||||
});
|
||||
export const forgetPasswordButton = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
});
|
||||
export const resendWrapper = style({
|
||||
height: 77,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 30,
|
||||
});
|
||||
export const sentRow = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '8px',
|
||||
lineHeight: '22px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
});
|
||||
export const sentMessage = style({
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontWeight: 600,
|
||||
});
|
||||
export const resendCountdown = style({
|
||||
width: 45,
|
||||
textAlign: 'center',
|
||||
});
|
||||
export const resendCountdownInButton = style({
|
||||
width: 40,
|
||||
textAlign: 'center',
|
||||
fontSize: cssVar('fontSm'),
|
||||
marginLeft: 16,
|
||||
color: cssVar('blue'),
|
||||
fontWeight: 400,
|
||||
});
|
||||
export const accessMessage = style({
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 500,
|
||||
marginTop: 65,
|
||||
marginBottom: 40,
|
||||
});
|
||||
export const userPlanButton = style({
|
||||
display: 'flex',
|
||||
fontSize: cssVar('fontXs'),
|
||||
@@ -114,44 +21,3 @@ export const userPlanButton = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const skipDivider = style({
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
height: 20,
|
||||
marginTop: 12,
|
||||
marginBottom: 12,
|
||||
});
|
||||
|
||||
export const skipDividerLine = style({
|
||||
flex: 1,
|
||||
height: 0,
|
||||
borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
});
|
||||
|
||||
export const skipDividerText = style({
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
|
||||
export const skipText = style({
|
||||
color: cssVarV2('text/primary'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 500,
|
||||
});
|
||||
|
||||
export const skipLink = style({
|
||||
color: cssVarV2('text/link'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
|
||||
export const skipLinkIcon = style({
|
||||
color: cssVarV2('text/link'),
|
||||
});
|
||||
|
||||
export const skipSection = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const loadingContainer = style({
|
||||
display: 'flex',
|
||||
width: '100vw',
|
||||
height: '60vh',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const subscriptionLayout = style({
|
||||
margin: '10% auto',
|
||||
maxWidth: '536px',
|
||||
});
|
||||
export const subscriptionBox = style({
|
||||
padding: '48px 52px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const subscriptionTips = style({
|
||||
margin: '20px 0',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: '12px',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
lineHeight: '20px',
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { Tabs, Tooltip } from '@affine/component';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Menu } from '@affine/component/ui/menu';
|
||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { LockIcon, PublishIcon } from '@blocksuite/icons/rc';
|
||||
@@ -121,7 +120,7 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
export const ShareMenu = (props: ShareMenuProps) => {
|
||||
const { workspaceMetadata } = props;
|
||||
|
||||
if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (workspaceMetadata.flavour === 'local') {
|
||||
return <LocalShareMenu {...props} />;
|
||||
}
|
||||
return <CloudShareMenu {...props} />;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -316,11 +315,9 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
||||
};
|
||||
|
||||
export const SharePage = (props: ShareMenuProps) => {
|
||||
if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (props.workspaceMetadata.flavour === 'local') {
|
||||
return <LocalSharePage {...props} />;
|
||||
} else if (
|
||||
props.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) {
|
||||
} else {
|
||||
return (
|
||||
// TODO(@eyhn): refactor this part
|
||||
<ErrorBoundary fallback={null}>
|
||||
@@ -330,5 +327,4 @@ export const SharePage = (props: ShareMenuProps) => {
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
throw new Error('Unreachable');
|
||||
};
|
||||
|
||||
@@ -1,48 +1,11 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
// modal atoms
|
||||
export const openWorkspacesModalAtom = atom(false);
|
||||
/**
|
||||
* @deprecated use `useSignOut` hook instated
|
||||
*/
|
||||
export const openQuotaModalAtom = atom(false);
|
||||
export const rightSidebarWidthAtom = atom(320);
|
||||
|
||||
export const openImportModalAtom = atom(false);
|
||||
|
||||
export type AuthAtomData =
|
||||
| { state: 'signIn' }
|
||||
| {
|
||||
state: 'afterSignUpSendEmail';
|
||||
email: string;
|
||||
}
|
||||
| {
|
||||
state: 'afterSignInSendEmail';
|
||||
email: string;
|
||||
}
|
||||
| {
|
||||
state: 'signInWithPassword';
|
||||
email: string;
|
||||
}
|
||||
| {
|
||||
state: 'sendEmail';
|
||||
email: string;
|
||||
emailType:
|
||||
| 'setPassword'
|
||||
| 'changePassword'
|
||||
| 'changeEmail'
|
||||
| 'verifyEmail';
|
||||
};
|
||||
|
||||
export const authAtom = atom<
|
||||
AuthAtomData & {
|
||||
openModal: boolean;
|
||||
}
|
||||
>({
|
||||
openModal: false,
|
||||
state: 'signIn',
|
||||
});
|
||||
|
||||
export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
|
||||
export const allPageFilterSelectAtom = atom<AllPageFilterOption>('docs');
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import type { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import {
|
||||
type getCopilotHistoriesQuery,
|
||||
type RequestOptions,
|
||||
} from '@affine/graphql';
|
||||
import { UnauthorizedError } from '@blocksuite/affine/blocks';
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import { getCurrentStore } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { CopilotClient } from './copilot-client';
|
||||
@@ -42,7 +41,10 @@ const processTypeToPromptName = new Map(
|
||||
// user-id:workspace-id:doc-id -> chat session id
|
||||
const chatSessions = new Map<string, Promise<string>>();
|
||||
|
||||
export function setupAIProvider(client: CopilotClient) {
|
||||
export function setupAIProvider(
|
||||
client: CopilotClient,
|
||||
globalDialogService: GlobalDialogService
|
||||
) {
|
||||
async function getChatSessionId(workspaceId: string, docId: string) {
|
||||
const userId = (await AIProvider.userInfo)?.id;
|
||||
|
||||
@@ -499,10 +501,7 @@ Could you make a new website based on these notes and send back just the html fi
|
||||
});
|
||||
|
||||
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.on(() => {
|
||||
getCurrentStore().set(authAtom, s => ({
|
||||
...s,
|
||||
openModal: true,
|
||||
}));
|
||||
globalDialogService.open('sign-in', {});
|
||||
});
|
||||
|
||||
setupTracker();
|
||||
|
||||
@@ -19,7 +19,6 @@ import { EditorService } from '@affine/core/modules/editor';
|
||||
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
@@ -111,7 +110,7 @@ export const PageHeaderMenuButton = ({
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
track.$.header.history.open();
|
||||
if (workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
if (workspace.flavour === 'affine-cloud') {
|
||||
return setHistoryModalOpen(true);
|
||||
}
|
||||
return setOpenHistoryTipsModal(true);
|
||||
@@ -398,8 +397,7 @@ export const PageHeaderMenuButton = ({
|
||||
data-testid="editor-option-menu-delete"
|
||||
onSelect={handleOpenTrashModal}
|
||||
/>
|
||||
{BUILD_CONFIG.isWeb &&
|
||||
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
||||
{BUILD_CONFIG.isWeb && workspace.flavour === 'affine-cloud' ? (
|
||||
<MenuItem
|
||||
prefixIcon={<LocalWorkspaceIcon />}
|
||||
data-testid="editor-option-menu-link"
|
||||
@@ -426,7 +424,7 @@ export const PageHeaderMenuButton = ({
|
||||
>
|
||||
<HeaderDropDownButton />
|
||||
</Menu>
|
||||
{workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
||||
{workspace.flavour !== 'local' ? (
|
||||
<PageHistoryModal
|
||||
docCollection={workspace.docCollection}
|
||||
open={historyModalOpen}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const SignIn = () => {
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const onClickSignIn = useCallback(() => {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}, [setOpen]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Avatar, PropertyValue } from '@affine/component';
|
||||
import { CloudDocMetaService } from '@affine/core/modules/cloud/services/cloud-doc-meta';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
@@ -73,8 +72,7 @@ const LocalUserValue = () => {
|
||||
|
||||
export const CreatedByValue = () => {
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const isCloud =
|
||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const isCloud = workspaceService.workspace.flavour !== 'local';
|
||||
|
||||
if (!isCloud) {
|
||||
return (
|
||||
@@ -93,8 +91,7 @@ export const CreatedByValue = () => {
|
||||
|
||||
export const UpdatedByValue = () => {
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const isCloud =
|
||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const isCloud = workspaceService.workspace.flavour !== 'local';
|
||||
|
||||
if (!isCloud) {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notify, useConfirmModal } from '@affine/component';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import {
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
useService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useNavigateHelper } from '../use-navigate-helper';
|
||||
@@ -31,7 +30,7 @@ export const useEnableCloud = () => {
|
||||
const authService = useService(AuthService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
const setAuthAtom = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
@@ -58,8 +57,8 @@ export const useEnableCloud = () => {
|
||||
);
|
||||
|
||||
const openSignIn = useCallback(() => {
|
||||
setAuthAtom(prev => ({ ...prev, openModal: true }));
|
||||
}, [setAuthAtom]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
const signInOrEnableCloud = useCallback(
|
||||
async (...args: ConfirmEnableArgs) => {
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { Editor } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { OpenInAppService } from '@affine/core/modules/open-in-app';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
@@ -78,7 +77,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
});
|
||||
}, [doc, openConfirmModal, t]);
|
||||
|
||||
const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const isCloudWorkspace = workspace.flavour !== 'local';
|
||||
|
||||
const openInAppService = useServiceOptional(OpenInAppService);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
} from '@affine/core/commands';
|
||||
import { useSharingUrl } from '@affine/core/components/hooks/affine/use-share-url';
|
||||
import { useIsActiveView } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { track } from '@affine/track';
|
||||
import { type WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
@@ -18,7 +17,7 @@ export function useRegisterCopyLinkCommands({
|
||||
}) {
|
||||
const isActiveView = useIsActiveView();
|
||||
const workspaceId = workspaceMeta.id;
|
||||
const isCloud = workspaceMeta.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const isCloud = workspaceMeta.flavour !== 'local';
|
||||
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
|
||||
@@ -3,8 +3,7 @@ import {
|
||||
notify,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { AuthService, ServerService } from '@affine/core/modules/cloud';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
GlobalContextService,
|
||||
@@ -32,18 +31,14 @@ export const useSignOut = ({
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { openPage } = useNavigateHelper();
|
||||
|
||||
const serverService = useService(ServerService);
|
||||
const authService = useService(AuthService);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const currentWorkspaceId = useLiveData(
|
||||
globalContextService.globalContext.workspaceId.$
|
||||
);
|
||||
const currentWorkspaceMetadata = useLiveData(
|
||||
currentWorkspaceId
|
||||
? workspacesService.list.workspace$(currentWorkspaceId)
|
||||
: undefined
|
||||
const currentWorkspaceFlavour = useLiveData(
|
||||
globalContextService.globalContext.workspaceFlavour.$
|
||||
);
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
@@ -58,10 +53,10 @@ export const useSignOut = ({
|
||||
});
|
||||
}
|
||||
|
||||
// if current workspace is affine cloud, switch to local workspace
|
||||
if (currentWorkspaceMetadata?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
// if current workspace is sign out, switch to other workspace
|
||||
if (currentWorkspaceFlavour === serverService.server.id) {
|
||||
const localWorkspace = workspaces.find(
|
||||
w => w.flavour === WorkspaceFlavour.LOCAL
|
||||
w => w.flavour !== serverService.server.id
|
||||
);
|
||||
if (localWorkspace) {
|
||||
openPage(localWorkspace.id, 'all');
|
||||
@@ -69,9 +64,10 @@ export const useSignOut = ({
|
||||
}
|
||||
}, [
|
||||
authService,
|
||||
currentWorkspaceMetadata?.flavour,
|
||||
currentWorkspaceFlavour,
|
||||
onConfirm,
|
||||
openPage,
|
||||
serverService.server.id,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
switchMap,
|
||||
timeout,
|
||||
} from 'rxjs';
|
||||
import { Map as YMap } from 'yjs';
|
||||
|
||||
import { CopilotClient } from '../blocksuite/block-suite-editor/ai/copilot-client';
|
||||
import { setupAIProvider } from '../blocksuite/block-suite-editor/ai/setup-provider';
|
||||
@@ -147,38 +146,18 @@ export const WorkspaceSideEffects = () => {
|
||||
graphqlService.gql,
|
||||
fetchService.fetch,
|
||||
eventSourceService.eventSource
|
||||
)
|
||||
),
|
||||
globalDialogService
|
||||
);
|
||||
return () => {
|
||||
dispose();
|
||||
};
|
||||
}, [eventSourceService, fetchService, graphqlService]);
|
||||
}, [eventSourceService, fetchService, globalDialogService, graphqlService]);
|
||||
|
||||
useRegisterWorkspaceCommands();
|
||||
useRegisterNavigationCommands();
|
||||
useRegisterFindInPageCommands();
|
||||
|
||||
useEffect(() => {
|
||||
// hotfix for blockVersions
|
||||
// this is a mistake in the
|
||||
// 0.8.0 ~ 0.8.1
|
||||
// 0.8.0-beta.0 ~ 0.8.0-beta.3
|
||||
// 0.8.0-canary.17 ~ 0.9.0-canary.3
|
||||
const meta = currentWorkspace.docCollection.doc.getMap('meta');
|
||||
const blockVersions = meta.get('blockVersions');
|
||||
if (
|
||||
!(blockVersions instanceof YMap) &&
|
||||
blockVersions !== null &&
|
||||
blockVersions !== undefined &&
|
||||
typeof blockVersions === 'object'
|
||||
) {
|
||||
meta.set(
|
||||
'blockVersions',
|
||||
new YMap(Object.entries(blockVersions as Record<string, number>))
|
||||
);
|
||||
}
|
||||
}, [currentWorkspace.docCollection.doc]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<QuickSearchContainer />
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type MenuProps,
|
||||
Skeleton,
|
||||
} from '@affine/component';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -17,7 +16,6 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
@@ -58,11 +56,11 @@ const AuthorizedUserInfo = ({ account }: { account: AuthAccountInfo }) => {
|
||||
};
|
||||
|
||||
const UnauthorizedUserInfo = () => {
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const openSignInModal = useCallback(() => {
|
||||
setOpen(state => ({ ...state, openModal: true }));
|
||||
}, [setOpen]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
||||
127
packages/frontend/core/src/components/sign-in/add-selfhosted.tsx
Normal file
127
packages/frontend/core/src/components/sign-in/add-selfhosted.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Button } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
AuthInput,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { ServersService } from '@affine/core/modules/cloud';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { SignInState } from '.';
|
||||
import * as styles from './style.css';
|
||||
|
||||
function normalizeURL(url: string) {
|
||||
const normalized = new URL(url).toString();
|
||||
return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
|
||||
}
|
||||
|
||||
export const AddSelfhostedStep = ({
|
||||
state,
|
||||
changeState,
|
||||
}: {
|
||||
state: SignInState;
|
||||
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||
}) => {
|
||||
const serversService = useService(ServersService);
|
||||
const [baseURL, setBaseURL] = useState(state.initialServerBaseUrl ?? '');
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const urlValid = useMemo(() => {
|
||||
try {
|
||||
normalizeURL(baseURL);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, [baseURL]);
|
||||
|
||||
const onBaseURLChange = useCallback((value: string) => {
|
||||
setBaseURL(value);
|
||||
setError(false);
|
||||
}, []);
|
||||
|
||||
const onConnect = useAsyncCallback(async () => {
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
const server = await serversService.addOrGetServerByBaseUrl(
|
||||
normalizeURL(baseURL)
|
||||
);
|
||||
changeState(prev => ({
|
||||
...prev,
|
||||
step: 'signIn',
|
||||
server,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(true);
|
||||
}
|
||||
|
||||
setIsConnecting(false);
|
||||
}, [baseURL, changeState, serversService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.initialServerBaseUrl) {
|
||||
changeState(prev => ({
|
||||
...prev,
|
||||
initialServerBaseUrl: undefined,
|
||||
}));
|
||||
onConnect();
|
||||
}
|
||||
}, [changeState, onConnect, state]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader title={t['com.affine.auth.sign.add-selfhosted']()} />
|
||||
<AuthContent>
|
||||
<AuthInput
|
||||
label={t['com.affine.auth.sign.add-selfhosted.baseurl']()}
|
||||
value={baseURL}
|
||||
onChange={onBaseURLChange}
|
||||
placeholder="https://your-server.com"
|
||||
error={!!error}
|
||||
disabled={isConnecting}
|
||||
errorHint={t['com.affine.auth.sign.add-selfhosted.error']()}
|
||||
onEnter={onConnect}
|
||||
/>
|
||||
<Button
|
||||
data-testid="connect-selfhosted-button"
|
||||
variant="primary"
|
||||
size="extraLarge"
|
||||
style={{ width: '100%', marginTop: '16px' }}
|
||||
disabled={!urlValid || isConnecting}
|
||||
loading={isConnecting}
|
||||
onClick={onConnect}
|
||||
>
|
||||
{t['com.affine.auth.sign.add-selfhosted.connect-button']()}
|
||||
</Button>
|
||||
<div className={styles.authMessage}>
|
||||
<Trans
|
||||
i18nKey="com.affine.auth.sign.add-selfhosted.description"
|
||||
components={{
|
||||
1: (
|
||||
<a
|
||||
href="https://docs.affine.pro/docs/self-host-affine"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AuthContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
62
packages/frontend/core/src/components/sign-in/index.tsx
Normal file
62
packages/frontend/core/src/components/sign-in/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { DefaultServerService, type Server } from '@affine/core/modules/cloud';
|
||||
import { FrameworkScope, useService } from '@toeverything/infra';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { AddSelfhostedStep } from './add-selfhosted';
|
||||
import { SignInStep } from './sign-in';
|
||||
import { SignInWithEmailStep } from './sign-in-with-email';
|
||||
import { SignInWithPasswordStep } from './sign-in-with-password';
|
||||
|
||||
export type SignInStep =
|
||||
| 'signIn'
|
||||
| 'signInWithPassword'
|
||||
| 'signInWithEmail'
|
||||
| 'addSelfhosted';
|
||||
|
||||
export interface SignInState {
|
||||
step: SignInStep;
|
||||
server?: Server;
|
||||
initialServerBaseUrl?: string;
|
||||
email?: string;
|
||||
redirectUrl?: string;
|
||||
}
|
||||
|
||||
export const SignInPanel = ({
|
||||
onClose,
|
||||
server: initialServerBaseUrl,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
server?: string;
|
||||
}) => {
|
||||
const [state, setState] = useState<SignInState>({
|
||||
step: initialServerBaseUrl ? 'addSelfhosted' : 'signIn',
|
||||
initialServerBaseUrl: initialServerBaseUrl,
|
||||
});
|
||||
|
||||
const defaultServerService = useService(DefaultServerService);
|
||||
|
||||
const step = state.step;
|
||||
const server = state.server ?? defaultServerService.server;
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={server.scope}>
|
||||
{step === 'signIn' ? (
|
||||
<SignInStep state={state} changeState={setState} close={onClose} />
|
||||
) : step === 'signInWithEmail' ? (
|
||||
<SignInWithEmailStep
|
||||
state={state}
|
||||
changeState={setState}
|
||||
close={onClose}
|
||||
/>
|
||||
) : step === 'signInWithPassword' ? (
|
||||
<SignInWithPasswordStep
|
||||
state={state}
|
||||
changeState={setState}
|
||||
close={onClose}
|
||||
/>
|
||||
) : step === 'addSelfhosted' ? (
|
||||
<AddSelfhostedStep state={state} changeState={setState} />
|
||||
) : null}
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
@@ -8,21 +8,38 @@ import {
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService, CaptchaService } from '@affine/core/modules/cloud';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { AuthPanelProps } from './index';
|
||||
import type { SignInState } from '.';
|
||||
import { Captcha } from './captcha';
|
||||
import * as style from './style.css';
|
||||
import { Captcha } from './use-captcha';
|
||||
|
||||
export const AfterSignInSendEmail = ({
|
||||
setAuthData: setAuth,
|
||||
email,
|
||||
redirectUrl,
|
||||
}: AuthPanelProps<'afterSignInSendEmail'>) => {
|
||||
export const SignInWithEmailStep = ({
|
||||
state,
|
||||
changeState,
|
||||
close,
|
||||
}: {
|
||||
state: SignInState;
|
||||
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||
close: () => void;
|
||||
}) => {
|
||||
const [resendCountDown, setResendCountDown] = useState(60);
|
||||
|
||||
const email = state.email;
|
||||
|
||||
if (!email) {
|
||||
throw new Unreachable();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setResendCountDown(c => Math.max(c - 1, 0));
|
||||
@@ -43,7 +60,20 @@ export const AfterSignInSendEmail = ({
|
||||
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
||||
const challenge = useLiveData(captchaService.challenge$);
|
||||
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'authenticated') {
|
||||
close();
|
||||
notify.success({
|
||||
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||
});
|
||||
}
|
||||
}, [close, loginStatus, t]);
|
||||
|
||||
const onResendClick = useAsyncCallback(async () => {
|
||||
if (isSending || (!verifyToken && needCaptcha)) return;
|
||||
setIsSending(true);
|
||||
try {
|
||||
setResendCountDown(60);
|
||||
@@ -52,7 +82,7 @@ export const AfterSignInSendEmail = ({
|
||||
email,
|
||||
verifyToken,
|
||||
challenge,
|
||||
redirectUrl
|
||||
state.redirectUrl
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -61,17 +91,34 @@ export const AfterSignInSendEmail = ({
|
||||
});
|
||||
}
|
||||
setIsSending(false);
|
||||
}, [authService, captchaService, challenge, email, redirectUrl, verifyToken]);
|
||||
}, [
|
||||
authService,
|
||||
captchaService,
|
||||
challenge,
|
||||
email,
|
||||
isSending,
|
||||
needCaptcha,
|
||||
state.redirectUrl,
|
||||
verifyToken,
|
||||
]);
|
||||
|
||||
const onSignInWithPasswordClick = useCallback(() => {
|
||||
setAuth({ state: 'signInWithPassword' });
|
||||
}, [setAuth]);
|
||||
changeState(prev => ({ ...prev, step: 'signInWithPassword' }));
|
||||
}, [changeState]);
|
||||
|
||||
const onBackBottomClick = useCallback(() => {
|
||||
setAuth({ state: 'signIn' });
|
||||
}, [setAuth]);
|
||||
changeState(prev => ({ ...prev, step: 'signIn' }));
|
||||
}, [changeState]);
|
||||
|
||||
return (
|
||||
return !verifyToken && needCaptcha ? (
|
||||
<>
|
||||
<ModalHeader title={t['com.affine.auth.sign.in']()} />
|
||||
<AuthContent style={{ height: 100 }}>
|
||||
<Captcha />
|
||||
</AuthContent>
|
||||
<BackButton onClick={onBackBottomClick} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
@@ -88,20 +135,14 @@ export const AfterSignInSendEmail = ({
|
||||
|
||||
<div className={style.resendWrapper}>
|
||||
{resendCountDown <= 0 ? (
|
||||
<>
|
||||
<Captcha />
|
||||
<Button
|
||||
style={
|
||||
!verifyToken && needCaptcha ? { cursor: 'not-allowed' } : {}
|
||||
}
|
||||
disabled={(!verifyToken && needCaptcha) || isSending}
|
||||
variant="plain"
|
||||
size="large"
|
||||
onClick={onResendClick}
|
||||
>
|
||||
{t['com.affine.auth.sign.auth.code.resend.hint']()}
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
disabled={isSending}
|
||||
variant="plain"
|
||||
size="large"
|
||||
onClick={onResendClick}
|
||||
>
|
||||
{t['com.affine.auth.sign.auth.code.resend.hint']()}
|
||||
</Button>
|
||||
) : (
|
||||
<div className={style.sentRow}>
|
||||
<div className={style.sentMessage}>
|
||||
@@ -6,27 +6,52 @@ import {
|
||||
} from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService, CaptchaService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
AuthService,
|
||||
CaptchaService,
|
||||
ServerService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { ServerDeploymentType } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AuthPanelProps } from './index';
|
||||
import type { SignInState } from '.';
|
||||
import { Captcha } from './captcha';
|
||||
import * as styles from './style.css';
|
||||
import { Captcha } from './use-captcha';
|
||||
|
||||
export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||
setAuthData,
|
||||
email,
|
||||
redirectUrl,
|
||||
export const SignInWithPasswordStep = ({
|
||||
state,
|
||||
changeState,
|
||||
close,
|
||||
}: {
|
||||
state: SignInState;
|
||||
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||
close: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const email = state.email;
|
||||
|
||||
if (!email) {
|
||||
throw new Unreachable();
|
||||
}
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
const captchaService = useService(CaptchaService);
|
||||
const serverService = useService(ServerService);
|
||||
const isSelfhosted = useLiveData(
|
||||
serverService.server.config$.selector(
|
||||
c => c.type === ServerDeploymentType.Selfhosted
|
||||
)
|
||||
);
|
||||
const serverName = useLiveData(
|
||||
serverService.server.config$.selector(c => c.serverName)
|
||||
);
|
||||
|
||||
const verifyToken = useLiveData(captchaService.verifyToken$);
|
||||
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
||||
@@ -34,8 +59,20 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [sendingEmail, setSendingEmail] = useState(false);
|
||||
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'authenticated') {
|
||||
close();
|
||||
notify.success({
|
||||
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||
});
|
||||
}
|
||||
}, [close, loginStatus, t]);
|
||||
|
||||
const onSignIn = useAsyncCallback(async () => {
|
||||
if (isLoading) return;
|
||||
if (isLoading || (!verifyToken && needCaptcha)) return;
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
@@ -55,6 +92,7 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||
}, [
|
||||
isLoading,
|
||||
verifyToken,
|
||||
needCaptcha,
|
||||
captchaService,
|
||||
authService,
|
||||
email,
|
||||
@@ -66,14 +104,7 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||
if (sendingEmail) return;
|
||||
setSendingEmail(true);
|
||||
try {
|
||||
captchaService.revalidate();
|
||||
await authService.sendEmailMagicLink(
|
||||
email,
|
||||
verifyToken,
|
||||
challenge,
|
||||
redirectUrl
|
||||
);
|
||||
setAuthData({ state: 'afterSignInSendEmail' });
|
||||
changeState(prev => ({ ...prev, step: 'signInWithEmail' }));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
@@ -82,26 +113,13 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||
// TODO(@eyhn): handle error better
|
||||
}
|
||||
setSendingEmail(false);
|
||||
}, [
|
||||
sendingEmail,
|
||||
verifyToken,
|
||||
captchaService,
|
||||
authService,
|
||||
email,
|
||||
challenge,
|
||||
redirectUrl,
|
||||
setAuthData,
|
||||
]);
|
||||
|
||||
const sendChangePasswordEmail = useCallback(() => {
|
||||
setAuthData({ state: 'sendEmail', emailType: 'changePassword' });
|
||||
}, [setAuthData]);
|
||||
}, [sendingEmail, changeState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={t['com.affine.brand.affineCloud']()}
|
||||
subTitle={serverName}
|
||||
/>
|
||||
|
||||
<Wrapper
|
||||
@@ -128,23 +146,8 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||
errorHint={t['com.affine.auth.password.error']()}
|
||||
onEnter={onSignIn}
|
||||
/>
|
||||
<div
|
||||
className={styles.forgetPasswordButtonRow}
|
||||
style={{ display: 'none' }} // Not implemented yet.
|
||||
>
|
||||
<a
|
||||
className={styles.linkButton}
|
||||
onClick={sendChangePasswordEmail}
|
||||
style={{
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
}}
|
||||
>
|
||||
{t['com.affine.auth.forget']()}
|
||||
</a>
|
||||
</div>
|
||||
{(verifyToken || !needCaptcha) && (
|
||||
<div className={styles.sendMagicLinkButtonRow}>
|
||||
{!isSelfhosted && (
|
||||
<div className={styles.passwordButtonRow}>
|
||||
<a
|
||||
data-testid="send-magic-link-button"
|
||||
className={styles.linkButton}
|
||||
@@ -168,8 +171,8 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||
</Wrapper>
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthData({ state: 'signIn' });
|
||||
}, [setAuthData])}
|
||||
changeState(prev => ({ ...prev, step: 'signIn' }));
|
||||
}, [changeState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
221
packages/frontend/core/src/components/sign-in/sign-in.tsx
Normal file
221
packages/frontend/core/src/components/sign-in/sign-in.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { Button, notify } from '@affine/component';
|
||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
||||
import { OAuth } from '@affine/core/components/affine/auth/oauth';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService, ServerService } from '@affine/core/modules/cloud';
|
||||
import { ServerDeploymentType } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { ArrowRightBigIcon, PublishIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { SignInState } from '.';
|
||||
import * as style from './style.css';
|
||||
|
||||
const emailRegex =
|
||||
/^(?:(?:[^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|((?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
function validateEmail(email: string) {
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
export const SignInStep = ({
|
||||
state,
|
||||
changeState,
|
||||
close,
|
||||
}: {
|
||||
state: SignInState;
|
||||
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||
close: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const serverService = useService(ServerService);
|
||||
const serverName = useLiveData(
|
||||
serverService.server.config$.selector(c => c.serverName)
|
||||
);
|
||||
const isSelfhosted = useLiveData(
|
||||
serverService.server.config$.selector(
|
||||
c => c.type === ServerDeploymentType.Selfhosted
|
||||
)
|
||||
);
|
||||
const authService = useService(AuthService);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableMultipleCloudServers = useLiveData(
|
||||
featureFlagService.flags.enable_multiple_cloud_servers.$
|
||||
);
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'authenticated') {
|
||||
close();
|
||||
notify.success({
|
||||
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||
});
|
||||
}
|
||||
}, [close, loginStatus, t]);
|
||||
|
||||
const onContinue = useAsyncCallback(async () => {
|
||||
if (!validateEmail(email)) {
|
||||
setIsValidEmail(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidEmail(true);
|
||||
setIsMutating(true);
|
||||
|
||||
try {
|
||||
const { hasPassword, registered, magicLink } =
|
||||
await authService.checkUserByEmail(email);
|
||||
|
||||
if (registered) {
|
||||
// provider password sign-in if user has by default
|
||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||
if (hasPassword) {
|
||||
changeState(prev => ({
|
||||
...prev,
|
||||
email,
|
||||
step: 'signInWithPassword',
|
||||
}));
|
||||
} else {
|
||||
if (magicLink) {
|
||||
changeState(prev => ({
|
||||
...prev,
|
||||
email,
|
||||
step: 'signInWithEmail',
|
||||
}));
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Failed to send email. Please contact the administrator.',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (magicLink) {
|
||||
changeState(prev => ({
|
||||
...prev,
|
||||
email,
|
||||
step: 'signInWithEmail',
|
||||
}));
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Failed to send email. Please contact the administrator.',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
// TODO(@eyhn): better error handling
|
||||
notify.error({
|
||||
title: 'Failed to send email. Please try again.',
|
||||
});
|
||||
}
|
||||
|
||||
setIsMutating(false);
|
||||
}, [authService, changeState, email]);
|
||||
|
||||
const onAddSelfhosted = useCallback(() => {
|
||||
changeState(prev => ({
|
||||
...prev,
|
||||
step: 'addSelfhosted',
|
||||
}));
|
||||
}, [changeState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={serverName}
|
||||
/>
|
||||
|
||||
<OAuth redirectUrl={state.redirectUrl} />
|
||||
|
||||
<div className={style.authModalContent}>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
||||
onChange={setEmail}
|
||||
error={!isValidEmail}
|
||||
errorHint={
|
||||
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
||||
}
|
||||
onEnter={onContinue}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{ width: '100%' }}
|
||||
size="extraLarge"
|
||||
data-testid="continue-login-button"
|
||||
block
|
||||
loading={isMutating}
|
||||
suffix={<ArrowRightBigIcon />}
|
||||
suffixStyle={{ width: 20, height: 20, color: cssVar('blue') }}
|
||||
onClick={onContinue}
|
||||
>
|
||||
{t['com.affine.auth.sign.email.continue']()}
|
||||
</Button>
|
||||
|
||||
{!isSelfhosted && (
|
||||
<div className={style.authMessage}>
|
||||
{/*prettier-ignore*/}
|
||||
<Trans i18nKey="com.affine.auth.sign.message">
|
||||
By clicking "Continue with Google/Email" above, you acknowledge that
|
||||
you agree to AFFiNE's <a href="https://affine.pro/terms" target="_blank" rel="noreferrer">Terms of Conditions</a> and <a href="https://affine.pro/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={style.skipDivider}>
|
||||
<div className={style.skipDividerLine} />
|
||||
<span className={style.skipDividerText}>or</span>
|
||||
<div className={style.skipDividerLine} />
|
||||
</div>
|
||||
<div className={style.skipSection}>
|
||||
{!isSelfhosted &&
|
||||
BUILD_CONFIG.isElectron &&
|
||||
enableMultipleCloudServers && (
|
||||
<Button
|
||||
variant="plain"
|
||||
className={style.addSelfhostedButton}
|
||||
prefix={
|
||||
<PublishIcon className={style.addSelfhostedButtonPrefix} />
|
||||
}
|
||||
onClick={onAddSelfhosted}
|
||||
>
|
||||
{t['com.affine.auth.sign.add-selfhosted']()}
|
||||
</Button>
|
||||
)}
|
||||
<div className={style.skipText}>
|
||||
{t['com.affine.mobile.sign-in.skip.hint']()}
|
||||
</div>
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={() => close()}
|
||||
className={style.skipLink}
|
||||
suffix={<ArrowRightBigIcon className={style.skipLinkIcon} />}
|
||||
>
|
||||
{t['com.affine.mobile.sign-in.skip.link']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
121
packages/frontend/core/src/components/sign-in/style.css.ts
Normal file
121
packages/frontend/core/src/components/sign-in/style.css.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const authModalContent = style({
|
||||
marginTop: '30px',
|
||||
});
|
||||
|
||||
export const authMessage = style({
|
||||
marginTop: '30px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: 1.5,
|
||||
});
|
||||
globalStyle(`${authMessage} a`, {
|
||||
color: cssVar('linkColor'),
|
||||
});
|
||||
globalStyle(`${authMessage} .link`, {
|
||||
cursor: 'pointer',
|
||||
color: cssVar('linkColor'),
|
||||
});
|
||||
|
||||
export const captchaWrapper = style({
|
||||
margin: 'auto',
|
||||
marginBottom: '4px',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
export const passwordButtonRow = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '30px',
|
||||
});
|
||||
|
||||
export const linkButton = style({
|
||||
color: cssVar('linkColor'),
|
||||
background: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '22px',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
export const resendWrapper = style({
|
||||
height: 77,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 30,
|
||||
});
|
||||
|
||||
export const sentRow = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '8px',
|
||||
lineHeight: '22px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
});
|
||||
|
||||
export const sentMessage = style({
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const resendCountdown = style({
|
||||
width: 45,
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
export const addSelfhostedButton = style({
|
||||
marginTop: 10,
|
||||
marginLeft: -5,
|
||||
marginBottom: 16,
|
||||
color: cssVarV2('text/link'),
|
||||
});
|
||||
|
||||
export const addSelfhostedButtonPrefix = style({
|
||||
color: cssVarV2('text/link'),
|
||||
});
|
||||
|
||||
export const skipDivider = style({
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
height: 20,
|
||||
marginTop: 12,
|
||||
marginBottom: 12,
|
||||
});
|
||||
|
||||
export const skipDividerLine = style({
|
||||
flex: 1,
|
||||
height: 0,
|
||||
borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
});
|
||||
|
||||
export const skipDividerText = style({
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
|
||||
export const skipText = style({
|
||||
color: cssVarV2('text/primary'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 500,
|
||||
});
|
||||
|
||||
export const skipLink = style({
|
||||
color: cssVarV2('text/link'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
|
||||
export const skipLinkIcon = style({
|
||||
color: cssVarV2('text/link'),
|
||||
});
|
||||
|
||||
export const skipSection = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
});
|
||||
@@ -1,13 +1,11 @@
|
||||
import { BrowserWarning, LocalDemoTips } from '@affine/component/affine-banner';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useEnableCloud } from '../components/hooks/affine/use-enable-cloud';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
import { authAtom } from './atoms';
|
||||
import { GlobalDialogService } from '../modules/dialogs';
|
||||
|
||||
const minimumChromeVersion = 106;
|
||||
|
||||
@@ -69,15 +67,15 @@ export const TopTip = ({
|
||||
const [showLocalDemoTips, setShowLocalDemoTips] = useState(true);
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const onLogin = useCallback(() => {
|
||||
setAuthModal({ openModal: true, state: 'signIn' });
|
||||
}, [setAuthModal]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
if (
|
||||
!BUILD_CONFIG.isElectron &&
|
||||
showLocalDemoTips &&
|
||||
workspace.flavour === WorkspaceFlavour.LOCAL
|
||||
workspace.flavour === 'local'
|
||||
) {
|
||||
return (
|
||||
<LocalDemoTips
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { MenuItem } from '@affine/component/ui/menu';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
type WorkspaceMetadata,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useCatchEventCallback } from '../../hooks/use-catch-event-hook';
|
||||
@@ -23,17 +21,14 @@ import { UserAccountItem } from './user-account';
|
||||
import { AFFiNEWorkspaceList } from './workspace-list';
|
||||
|
||||
export const SignInItem = () => {
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const onClickSignIn = useCallback(() => {
|
||||
track.$.navigationPanel.workspaceList.requestSignIn();
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}, [setOpen]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
@@ -83,14 +78,9 @@ const UserWithWorkspaceListInner = ({
|
||||
|
||||
const isAuthenticated = session.status === 'authenticated';
|
||||
|
||||
const setOpenSignIn = useSetAtom(authAtom);
|
||||
|
||||
const openSignInModal = useCallback(() => {
|
||||
setOpenSignIn(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}, [setOpenSignIn]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
const onNewWorkspace = useCallback(() => {
|
||||
if (
|
||||
|
||||
@@ -12,11 +12,18 @@ export const workspaceListWrapper = style({
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
});
|
||||
export const workspaceType = style({
|
||||
export const workspaceServer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
padding: '0px 12px',
|
||||
});
|
||||
|
||||
export const workspaceServerName = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ScrollableContainer,
|
||||
} from '@affine/component';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import type { Server } from '@affine/core/modules/cloud';
|
||||
import { AuthService, ServersService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
CloudWorkspaceIcon,
|
||||
LocalWorkspaceIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
FrameworkScope,
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
@@ -29,28 +42,97 @@ interface WorkspaceModalProps {
|
||||
}
|
||||
|
||||
const CloudWorkSpaceList = ({
|
||||
server,
|
||||
workspaces,
|
||||
onClickWorkspace,
|
||||
onClickWorkspaceSetting,
|
||||
}: Omit<WorkspaceModalProps, 'onNewWorkspace' | 'onAddWorkspace'>) => {
|
||||
const t = useI18n();
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
}
|
||||
onClickEnableCloud,
|
||||
}: {
|
||||
server: Server;
|
||||
workspaces: WorkspaceMetadata[];
|
||||
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onClickWorkspaceSetting?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
|
||||
}) => {
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||
const authService = useService(AuthService);
|
||||
const serversService = useService(ServersService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
const accountStatus = useLiveData(authService.session.status$);
|
||||
const navigateHelper = useNavigateHelper();
|
||||
|
||||
const currentWorkspaceFlavour = useLiveData(
|
||||
globalContextService.globalContext.workspaceFlavour.$
|
||||
);
|
||||
|
||||
const handleDeleteServer = useCallback(() => {
|
||||
serversService.removeServer(server.id);
|
||||
|
||||
if (currentWorkspaceFlavour === server.id) {
|
||||
const otherWorkspace = workspaces.find(w => w.flavour !== server.id);
|
||||
if (otherWorkspace) {
|
||||
navigateHelper.openPage(otherWorkspace.id, 'all');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentWorkspaceFlavour,
|
||||
navigateHelper,
|
||||
server.id,
|
||||
serversService,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
const handleSignOut = useAsyncCallback(async () => {
|
||||
await authService.signOut();
|
||||
}, [authService]);
|
||||
|
||||
const handleSignIn = useAsyncCallback(async () => {
|
||||
globalDialogService.open('sign-in', {
|
||||
server: server.baseUrl,
|
||||
});
|
||||
}, [globalDialogService, server.baseUrl]);
|
||||
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
<div className={styles.workspaceType}>
|
||||
<CloudWorkspaceIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className={styles.workspaceTypeIcon}
|
||||
/>
|
||||
{t['com.affine.workspaceList.workspaceListType.cloud']()}
|
||||
<div className={styles.workspaceServer}>
|
||||
<div className={styles.workspaceServerName}>
|
||||
<CloudWorkspaceIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className={styles.workspaceTypeIcon}
|
||||
/>
|
||||
{serverName} -
|
||||
{account ? account.email : 'Not signed in'}
|
||||
</div>
|
||||
<Menu
|
||||
items={[
|
||||
server.id !== 'affine-cloud' && (
|
||||
<MenuItem key="delete-server" onClick={handleDeleteServer}>
|
||||
Delete Server
|
||||
</MenuItem>
|
||||
),
|
||||
accountStatus === 'authenticated' && (
|
||||
<MenuItem key="sign-out" onClick={handleSignOut}>
|
||||
Sign Out
|
||||
</MenuItem>
|
||||
),
|
||||
accountStatus === 'unauthenticated' && (
|
||||
<MenuItem key="sign-in" onClick={handleSignIn}>
|
||||
Sign In
|
||||
</MenuItem>
|
||||
),
|
||||
]}
|
||||
>
|
||||
<IconButton icon={<MoreHorizontalIcon />} />
|
||||
</Menu>
|
||||
</div>
|
||||
<WorkspaceList
|
||||
items={workspaces}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onEnableCloudClick={onClickEnableCloud}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -68,13 +150,15 @@ const LocalWorkspaces = ({
|
||||
}
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
<div className={styles.workspaceType}>
|
||||
<LocalWorkspaceIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className={styles.workspaceTypeIcon}
|
||||
/>
|
||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
||||
<div className={styles.workspaceServer}>
|
||||
<div className={styles.workspaceServerName}>
|
||||
<LocalWorkspaceIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className={styles.workspaceTypeIcon}
|
||||
/>
|
||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceList
|
||||
items={workspaces}
|
||||
@@ -103,15 +187,13 @@ export const AFFiNEWorkspaceList = ({
|
||||
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const session = useService(AuthService).session;
|
||||
const status = useLiveData(session.status$);
|
||||
|
||||
const isAuthenticated = status === 'authenticated';
|
||||
const serversService = useService(ServersService);
|
||||
const servers = useLiveData(serversService.servers$);
|
||||
|
||||
const cloudWorkspaces = useMemo(
|
||||
() =>
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
({ flavour }) => flavour !== 'local'
|
||||
) as WorkspaceMetadata[],
|
||||
[workspaces]
|
||||
);
|
||||
@@ -119,7 +201,7 @@ export const AFFiNEWorkspaceList = ({
|
||||
const localWorkspaces = useMemo(
|
||||
() =>
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
||||
({ flavour }) => flavour === 'local'
|
||||
) as WorkspaceMetadata[],
|
||||
[workspaces]
|
||||
);
|
||||
@@ -160,20 +242,23 @@ export const AFFiNEWorkspaceList = ({
|
||||
className={styles.workspaceListsWrapper}
|
||||
scrollBarClassName={styles.scrollbar}
|
||||
>
|
||||
{isAuthenticated ? (
|
||||
<div>
|
||||
<CloudWorkSpaceList
|
||||
workspaces={cloudWorkspaces}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
onClickWorkspaceSetting={
|
||||
showSettingsButton ? onClickWorkspaceSetting : undefined
|
||||
}
|
||||
/>
|
||||
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
||||
<div>
|
||||
{servers.map(server => (
|
||||
<FrameworkScope key={server.id} scope={server.scope}>
|
||||
<CloudWorkSpaceList
|
||||
server={server}
|
||||
workspaces={cloudWorkspaces.filter(
|
||||
({ flavour }) => flavour === server.id
|
||||
)}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
onClickWorkspaceSetting={
|
||||
showSettingsButton ? onClickWorkspaceSetting : undefined
|
||||
}
|
||||
/>
|
||||
<Divider size="thinner" />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</FrameworkScope>
|
||||
))}
|
||||
</div>
|
||||
<LocalWorkspaces
|
||||
workspaces={localWorkspaces}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useSystemOnline } from '@affine/core/components/hooks/use-system-online
|
||||
import { useWorkspace } from '@affine/core/components/hooks/use-workspace';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
CloudWorkspaceIcon,
|
||||
@@ -97,7 +96,7 @@ const useSyncEngineSyncProgress = (meta: WorkspaceMetadata) => {
|
||||
|
||||
let content;
|
||||
// TODO(@eyhn): add i18n
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (workspace.flavour === 'local') {
|
||||
if (!BUILD_CONFIG.isElectron) {
|
||||
content = 'This is a local demo workspace.';
|
||||
} else {
|
||||
@@ -132,7 +131,7 @@ const useSyncEngineSyncProgress = (meta: WorkspaceMetadata) => {
|
||||
return {
|
||||
message: content,
|
||||
icon:
|
||||
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
||||
workspace.flavour !== 'local' ? (
|
||||
!isOnline ? (
|
||||
<OfflineStatus />
|
||||
) : (
|
||||
@@ -143,7 +142,7 @@ const useSyncEngineSyncProgress = (meta: WorkspaceMetadata) => {
|
||||
),
|
||||
progress,
|
||||
active:
|
||||
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD &&
|
||||
workspace.flavour !== 'local' &&
|
||||
((syncing && progress !== undefined) || engineState.retrying), // active if syncing or retrying,
|
||||
};
|
||||
};
|
||||
@@ -173,7 +172,7 @@ const WorkspaceSyncInfo = ({
|
||||
workspaceProfile: WorkspaceProfileInfo;
|
||||
}) => {
|
||||
const syncStatus = useSyncEngineSyncProgress(workspaceMetadata);
|
||||
const isCloud = workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const isCloud = workspaceMetadata.flavour !== 'local';
|
||||
const { paused, pause } = usePauseAnimation();
|
||||
|
||||
// to make sure that animation will play first time
|
||||
@@ -315,8 +314,7 @@ export const WorkspaceCard = forwardRef<
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.showOnCardHover}>
|
||||
{onClickEnableCloud &&
|
||||
workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
{onClickEnableCloud && workspaceMetadata.flavour === 'local' ? (
|
||||
<Button
|
||||
className={styles.enableCloudButton}
|
||||
onClick={onEnableCloud}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Button, Modal, notify, Wrapper } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
AuthInput,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import {
|
||||
AuthService,
|
||||
DefaultServerService,
|
||||
ServersService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import {
|
||||
sendChangePasswordEmailMutation,
|
||||
sendSetPasswordEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const ChangePasswordDialog = ({
|
||||
close,
|
||||
server: serverBaseUrl,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['change-password']>) => {
|
||||
const t = useI18n();
|
||||
const defaultServerService = useService(DefaultServerService);
|
||||
const serversService = useService(ServersService);
|
||||
let server;
|
||||
|
||||
if (serverBaseUrl) {
|
||||
server = serversService.getServerByBaseUrl(serverBaseUrl);
|
||||
if (!server) {
|
||||
throw new Unreachable('Server not found');
|
||||
}
|
||||
} else {
|
||||
server = defaultServerService.server;
|
||||
}
|
||||
|
||||
const authService = server.scope.get(AuthService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
const email = account?.email;
|
||||
const hasPassword = account?.info?.hasPassword;
|
||||
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const passwordLimits = useLiveData(
|
||||
server.credentialsRequirement$.map(r => r?.password)
|
||||
);
|
||||
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||
|
||||
if (!email) {
|
||||
// should not happen
|
||||
throw new Unreachable();
|
||||
}
|
||||
|
||||
const onSendEmail = useAsyncCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (hasPassword) {
|
||||
await server.gql({
|
||||
query: sendChangePasswordEmailMutation,
|
||||
variables: {
|
||||
callbackUrl: `/auth/changePassword`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await server.gql({
|
||||
query: sendSetPasswordEmailMutation,
|
||||
variables: {
|
||||
callbackUrl: `/auth/setPassword`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
notify.success({
|
||||
title: hasPassword
|
||||
? t['com.affine.auth.sent.change.password.hint']()
|
||||
: t['com.affine.auth.sent.set.password.hint'](),
|
||||
});
|
||||
setHasSentEmail(true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
title: t['com.affine.auth.sent.change.email.fail'](),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [hasPassword, server, t]);
|
||||
|
||||
if (!passwordLimits) {
|
||||
// TODO(@eyhn): loading & error UI
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={() => close()}
|
||||
width={400}
|
||||
minHeight={500}
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'change-password-modal',
|
||||
style: { padding: '44px 40px 20px' },
|
||||
}}
|
||||
>
|
||||
<ModalHeader
|
||||
title={serverName}
|
||||
subTitle={
|
||||
hasPassword
|
||||
? t['com.affine.auth.reset.password']()
|
||||
: t['com.affine.auth.set.password']()
|
||||
}
|
||||
/>
|
||||
<AuthContent>
|
||||
{hasPassword
|
||||
? t['com.affine.auth.reset.password.message']()
|
||||
: t['com.affine.auth.set.password.message']({
|
||||
min: String(passwordLimits.minLength),
|
||||
max: String(passwordLimits.maxLength),
|
||||
})}
|
||||
</AuthContent>
|
||||
|
||||
<Wrapper
|
||||
marginTop={30}
|
||||
marginBottom={50}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
disabled={true}
|
||||
value={email}
|
||||
/>
|
||||
</Wrapper>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="extraLarge"
|
||||
style={{ width: '100%' }}
|
||||
disabled={hasSentEmail}
|
||||
loading={loading}
|
||||
onClick={onSendEmail}
|
||||
>
|
||||
{hasSentEmail
|
||||
? t['com.affine.auth.sent']()
|
||||
: hasPassword
|
||||
? t['com.affine.auth.send.reset.password.link']()
|
||||
: t['com.affine.auth.send.set.password.link']()}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Avatar, ConfirmModal, Input, Switch } from '@affine/component';
|
||||
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||
import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
GlobalDialogService,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
useService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
|
||||
@@ -27,7 +25,7 @@ interface NameWorkspaceContentProps extends ConfirmModalProps {
|
||||
loading: boolean;
|
||||
onConfirmName: (
|
||||
name: string,
|
||||
workspaceFlavour: WorkspaceFlavour,
|
||||
workspaceFlavour: string,
|
||||
avatar?: File
|
||||
) => void;
|
||||
}
|
||||
@@ -47,14 +45,11 @@ const NameWorkspaceContent = ({
|
||||
const session = useService(AuthService).session;
|
||||
const loginStatus = useLiveData(session.status$);
|
||||
|
||||
const setOpenSignIn = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const openSignInModal = useCallback(() => {
|
||||
setOpenSignIn(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}, [setOpenSignIn]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
const onSwitchChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
@@ -67,10 +62,7 @@ const NameWorkspaceContent = ({
|
||||
);
|
||||
|
||||
const handleCreateWorkspace = useCallback(() => {
|
||||
onConfirmName(
|
||||
workspaceName,
|
||||
enable ? WorkspaceFlavour.AFFINE_CLOUD : WorkspaceFlavour.LOCAL
|
||||
);
|
||||
onConfirmName(workspaceName, enable ? 'affine-cloud' : 'local');
|
||||
}, [enable, onConfirmName, workspaceName]);
|
||||
|
||||
const onEnter = useCallback(() => {
|
||||
@@ -161,7 +153,7 @@ export const CreateWorkspaceDialog = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onConfirmName = useAsyncCallback(
|
||||
async (name: string, workspaceFlavour: WorkspaceFlavour) => {
|
||||
async (name: string, workspaceFlavour: string) => {
|
||||
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
ImportTemplateService,
|
||||
TemplateDownloaderService,
|
||||
} from '@affine/core/modules/import-template';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import { AllDocsIcon } from '@blocksuite/icons/rc';
|
||||
@@ -56,7 +55,7 @@ const Dialog = ({
|
||||
useState<WorkspaceMetadata | null>(null);
|
||||
const selectedWorkspace =
|
||||
rawSelectedWorkspace ??
|
||||
workspaces.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
|
||||
workspaces.find(w => w.flavour !== 'local') ??
|
||||
workspaces.at(0);
|
||||
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
|
||||
const { openPage, jumpToSignIn } = useNavigateHelper();
|
||||
@@ -146,7 +145,8 @@ const Dialog = ({
|
||||
try {
|
||||
const { workspaceId, docId } =
|
||||
await importTemplateService.importToNewWorkspace(
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
// TODO: support selfhosted
|
||||
'affine-cloud',
|
||||
'Workspace',
|
||||
templateDownloader.data$.value
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
import { _addLocalWorkspace } from '@affine/core/modules/workspace-engine';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
||||
import { useLayoutEffect } from 'react';
|
||||
@@ -37,7 +36,7 @@ export const ImportWorkspaceDialog = ({
|
||||
workspacesService.list.revalidate();
|
||||
close({
|
||||
workspace: {
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
flavour: 'local',
|
||||
id: result.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AuthModal } from '@affine/core/components/affine/auth';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import { ChangePasswordDialog } from './change-password';
|
||||
import { CollectionEditorDialog } from './collection-editor';
|
||||
import { CreateWorkspaceDialog } from './create-workspace';
|
||||
import { DocInfoDialog } from './doc-info';
|
||||
@@ -19,12 +19,17 @@ import { DateSelectorDialog } from './selectors/date';
|
||||
import { DocSelectorDialog } from './selectors/doc';
|
||||
import { TagSelectorDialog } from './selectors/tag';
|
||||
import { SettingDialog } from './setting';
|
||||
import { SignInDialog } from './sign-in';
|
||||
import { VerifyEmailDialog } from './verify-email';
|
||||
|
||||
const GLOBAL_DIALOGS = {
|
||||
'create-workspace': CreateWorkspaceDialog,
|
||||
'import-workspace': ImportWorkspaceDialog,
|
||||
'import-template': ImportTemplateDialog,
|
||||
setting: SettingDialog,
|
||||
'sign-in': SignInDialog,
|
||||
'change-password': ChangePasswordDialog,
|
||||
'verify-email': VerifyEmailDialog,
|
||||
} satisfies {
|
||||
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
||||
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
||||
@@ -66,8 +71,6 @@ export const GlobalDialogs = () => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<AuthModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { Upload } from '@affine/core/components/pure/file-upload';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { AuthService, ServerService } from '../../../../modules/cloud';
|
||||
@@ -178,9 +177,10 @@ export const AccountSetting = ({
|
||||
}: {
|
||||
onChangeSettingState?: (settingState: SettingState) => void;
|
||||
}) => {
|
||||
const { authService, serverService } = useServices({
|
||||
const { authService, serverService, globalDialogService } = useServices({
|
||||
AuthService,
|
||||
ServerService,
|
||||
GlobalDialogService,
|
||||
});
|
||||
const serverFeatures = useLiveData(serverService.server.features$);
|
||||
const t = useI18n();
|
||||
@@ -189,28 +189,20 @@ export const AccountSetting = ({
|
||||
session.revalidate();
|
||||
}, [session]);
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
const openSignOutModal = useSignOut();
|
||||
|
||||
const onChangeEmail = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
// @ts-expect-error accont email is always defined
|
||||
email: account.email,
|
||||
emailType: account.info?.emailVerified ? 'changeEmail' : 'verifyEmail',
|
||||
globalDialogService.open('verify-email', {
|
||||
server: serverService.server.baseUrl,
|
||||
changeEmail: !!account.info?.emailVerified,
|
||||
});
|
||||
}, [account.email, account.info?.emailVerified, setAuthModal]);
|
||||
}, [account, globalDialogService, serverService.server.baseUrl]);
|
||||
|
||||
const onPasswordButtonClick = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
// @ts-expect-error accont email is always defined
|
||||
email: account.email,
|
||||
emailType: account.info?.hasPassword ? 'changePassword' : 'setPassword',
|
||||
globalDialogService.open('change-password', {
|
||||
server: serverService.server.baseUrl,
|
||||
});
|
||||
}, [account.email, account.info?.hasPassword, setAuthModal]);
|
||||
}, [globalDialogService, serverService.server.baseUrl]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { Button, type ButtonProps } from '@affine/component';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const AILogin = (btnProps: ButtonProps) => {
|
||||
const t = useI18n();
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const onClickSignIn = useCallback(() => {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}, [setOpen]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
return (
|
||||
<Button onClick={onClickSignIn} variant="primary" {...btnProps}>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Button, type ButtonProps } from '@affine/component/ui/button';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import {
|
||||
type CreateCheckoutSessionInput,
|
||||
SubscriptionRecurring,
|
||||
@@ -18,7 +18,6 @@ import { track } from '@affine/track';
|
||||
import { DoneIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@@ -387,14 +386,11 @@ export const SignUpAction = ({
|
||||
children,
|
||||
className,
|
||||
}: PropsWithChildren<{ className?: string }>) => {
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const onClickSignIn = useCallback(() => {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}, [setOpen]);
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { UserPlanButton } from '@affine/core/components/affine/auth/user-plan-button';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import {
|
||||
type MouseEvent,
|
||||
Suspense,
|
||||
@@ -95,14 +94,14 @@ export const UserInfo = ({
|
||||
|
||||
export const SignInButton = () => {
|
||||
const t = useI18n();
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={style.accountButton}
|
||||
onClick={useCallback(() => {
|
||||
setAuthModal({ openModal: true, state: 'signIn' });
|
||||
}, [setAuthModal])}
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService])}
|
||||
>
|
||||
<div className="avatar not-sign">
|
||||
<Logo1Icon />
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
@@ -44,7 +43,7 @@ export const WorkspaceDeleteModal = ({
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
{workspaceMetadata.flavour === 'local' ? (
|
||||
<Trans i18nKey="com.affine.workspaceDelete.description">
|
||||
Deleting (
|
||||
<span className={styles.workspaceName}>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
useLiveData,
|
||||
@@ -37,7 +36,7 @@ export const EnableCloudPanel = ({
|
||||
});
|
||||
}, [confirmEnableCloud, onCloseSetting, workspace]);
|
||||
|
||||
if (flavour !== WorkspaceFlavour.LOCAL) {
|
||||
if (flavour !== 'local') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
WorkspacePermissionService,
|
||||
} from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Permission, UserFriendlyError } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -410,7 +409,7 @@ export const MembersPanel = ({
|
||||
onChangeSettingState: (settingState: SettingState) => void;
|
||||
}): ReactElement | null => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (workspace.flavour === 'local') {
|
||||
return <MembersPanelLocal />;
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -6,13 +6,12 @@ import {
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceShareSettingService } from '@affine/core/modules/share-setting';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
|
||||
export const SharingPanel = () => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (workspace.flavour === 'local') {
|
||||
return null;
|
||||
}
|
||||
return <Sharing />;
|
||||
|
||||
25
packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx
Normal file
25
packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Modal } from '@affine/component';
|
||||
import { SignInPanel } from '@affine/core/components/sign-in';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
export const SignInDialog = ({
|
||||
close,
|
||||
server: initialServerBaseUrl,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['sign-in']>) => {
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={() => close()}
|
||||
width={400}
|
||||
minHeight={500}
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'auth-modal',
|
||||
style: { padding: '44px 40px 20px' },
|
||||
}}
|
||||
>
|
||||
<SignInPanel onClose={close} server={initialServerBaseUrl} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Button, Modal, notify, Wrapper } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
AuthInput,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import {
|
||||
AuthService,
|
||||
DefaultServerService,
|
||||
ServersService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import {
|
||||
sendChangeEmailMutation,
|
||||
sendVerifyEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const VerifyEmailDialog = ({
|
||||
close,
|
||||
server: serverBaseUrl,
|
||||
changeEmail,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['verify-email']>) => {
|
||||
const t = useI18n();
|
||||
const defaultServerService = useService(DefaultServerService);
|
||||
const serversService = useService(ServersService);
|
||||
let server;
|
||||
|
||||
if (serverBaseUrl) {
|
||||
server = serversService.getServerByBaseUrl(serverBaseUrl);
|
||||
if (!server) {
|
||||
throw new Unreachable('Server not found');
|
||||
}
|
||||
} else {
|
||||
server = defaultServerService.server;
|
||||
}
|
||||
|
||||
const authService = server.scope.get(AuthService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
const email = account?.email;
|
||||
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const passwordLimits = useLiveData(
|
||||
server.credentialsRequirement$.map(r => r?.password)
|
||||
);
|
||||
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||
|
||||
if (!email) {
|
||||
// should not happen
|
||||
throw new Unreachable();
|
||||
}
|
||||
|
||||
const onSendEmail = useAsyncCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (changeEmail) {
|
||||
await server.gql({
|
||||
query: sendChangeEmailMutation,
|
||||
variables: {
|
||||
callbackUrl: `/auth/changeEmail`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await server.gql({
|
||||
query: sendVerifyEmailMutation,
|
||||
variables: {
|
||||
callbackUrl: `/auth/verify-email`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
notify.success({
|
||||
title: t['com.affine.auth.send.verify.email.hint'](),
|
||||
});
|
||||
setHasSentEmail(true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
title: t['com.affine.auth.sent.change.email.fail'](),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [changeEmail, server, t]);
|
||||
|
||||
if (!passwordLimits) {
|
||||
// should never reach here
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={() => close()}
|
||||
width={400}
|
||||
minHeight={500}
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'verify-email-modal',
|
||||
style: { padding: '44px 40px 20px' },
|
||||
}}
|
||||
>
|
||||
<ModalHeader
|
||||
title={serverName}
|
||||
subTitle={t['com.affine.settings.email.action.change']()}
|
||||
/>
|
||||
<AuthContent>
|
||||
{t['com.affine.auth.verify.email.message']({ email })}
|
||||
</AuthContent>
|
||||
|
||||
<Wrapper
|
||||
marginTop={30}
|
||||
marginBottom={50}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
disabled={true}
|
||||
value={email}
|
||||
/>
|
||||
</Wrapper>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="extraLarge"
|
||||
style={{ width: '100%' }}
|
||||
disabled={hasSentEmail}
|
||||
loading={loading}
|
||||
onClick={onSendEmail}
|
||||
>
|
||||
{hasSentEmail
|
||||
? t['com.affine.auth.sent']()
|
||||
: t['com.affine.auth.send.verify.email.hint']()}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
||||
import { SignInPageContainer } from '@affine/component/auth-components';
|
||||
import { SignInPanel } from '@affine/core/components/sign-in';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { AuthPanel } from '../../../components/affine/auth';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
@@ -17,33 +19,39 @@ export const SignIn = ({
|
||||
}: {
|
||||
redirectUrl?: string;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const session = useService(AuthService).session;
|
||||
const status = useLiveData(session.status$);
|
||||
const isRevalidating = useLiveData(session.isRevalidating$);
|
||||
const navigate = useNavigate();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const [searchParams] = useSearchParams();
|
||||
const isLoggedIn = status === 'authenticated' && !isRevalidating;
|
||||
const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri');
|
||||
const error = searchParams.get('error');
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
if (redirectUrl) {
|
||||
navigate(redirectUrl, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE, {
|
||||
search: searchParams.toString(),
|
||||
});
|
||||
}
|
||||
if (error) {
|
||||
notify.error({
|
||||
title: t['com.affine.auth.toast.title.failed'](),
|
||||
message: error,
|
||||
});
|
||||
}
|
||||
}, [jumpToIndex, navigate, isLoggedIn, redirectUrl, searchParams]);
|
||||
}, [error, t]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (session.status$.value === 'authenticated' && redirectUrl) {
|
||||
navigate(redirectUrl, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE, {
|
||||
search: searchParams.toString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SignInPageContainer>
|
||||
<div style={{ maxWidth: '400px', width: '100%' }}>
|
||||
<AuthPanel onSkip={jumpToIndex} redirectUrl={redirectUrl} />
|
||||
<SignInPanel onClose={handleClose} />
|
||||
</div>
|
||||
</SignInPageContainer>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
buildShowcaseWorkspace,
|
||||
createFirstAppData,
|
||||
} from '@affine/core/utils/first-app-data';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
@@ -59,11 +58,8 @@ export const Component = ({
|
||||
const createCloudWorkspace = useCallback(() => {
|
||||
if (createOnceRef.current) return;
|
||||
createOnceRef.current = true;
|
||||
buildShowcaseWorkspace(
|
||||
workspacesService,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
'AFFiNE Cloud'
|
||||
)
|
||||
// TODO: support selfhosted
|
||||
buildShowcaseWorkspace(workspacesService, 'affine-cloud', 'AFFiNE Cloud')
|
||||
.then(({ meta, defaultDocId }) => {
|
||||
if (defaultDocId) {
|
||||
jumpToPage(meta.id, defaultDocId);
|
||||
@@ -86,15 +82,14 @@ export const Component = ({
|
||||
// check is user logged in && has cloud workspace
|
||||
if (searchParams.get('initCloud') === 'true') {
|
||||
if (loggedIn) {
|
||||
if (list.every(w => w.flavour !== WorkspaceFlavour.AFFINE_CLOUD)) {
|
||||
if (list.every(w => w.flavour !== 'affine-cloud')) {
|
||||
createCloudWorkspace();
|
||||
return;
|
||||
}
|
||||
|
||||
// open first cloud workspace
|
||||
const openWorkspace =
|
||||
list.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
|
||||
list[0];
|
||||
list.find(w => w.flavour === 'affine-cloud') ?? list[0];
|
||||
openPage(openWorkspace.id, defaultIndexRoute);
|
||||
} else {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { NotificationCenter } from '@affine/component';
|
||||
import { DefaultServerService } from '@affine/core/modules/cloud';
|
||||
import { FrameworkScope, useService } from '@toeverything/infra';
|
||||
import {
|
||||
FrameworkScope,
|
||||
GlobalContextService,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
@@ -10,6 +14,7 @@ import { FindInPageModal } from './find-in-page/find-in-page-modal';
|
||||
|
||||
export const RootWrapper = () => {
|
||||
const defaultServerService = useService(DefaultServerService);
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const [isServerReady, setIsServerReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,6 +35,15 @@ export const RootWrapper = () => {
|
||||
};
|
||||
}, [defaultServerService, isServerReady]);
|
||||
|
||||
useEffect(() => {
|
||||
globalContextService.globalContext.serverId.set(
|
||||
defaultServerService.server.id
|
||||
);
|
||||
return () => {
|
||||
globalContextService.globalContext.serverId.set(null);
|
||||
};
|
||||
}, [defaultServerService, globalContextService]);
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={defaultServerService.server.scope}>
|
||||
<GlobalDialogs />
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
||||
import { workbenchRoutes } from '@affine/core/desktop/workbench-router';
|
||||
import {
|
||||
DefaultServerService,
|
||||
WorkspaceServerService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { ZipTransformer } from '@blocksuite/affine/blocks';
|
||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
@@ -125,12 +129,15 @@ export const Component = (): ReactElement => {
|
||||
};
|
||||
|
||||
const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
||||
const { workspacesService, globalContextService } = useServices({
|
||||
WorkspacesService,
|
||||
GlobalContextService,
|
||||
});
|
||||
const { workspacesService, globalContextService, defaultServerService } =
|
||||
useServices({
|
||||
WorkspacesService,
|
||||
GlobalContextService,
|
||||
DefaultServerService,
|
||||
});
|
||||
|
||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||
const workspaceServer = workspace?.scope.get(WorkspaceServerService).server;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ref = workspacesService.open({ metadata: meta });
|
||||
@@ -189,13 +196,30 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
||||
};
|
||||
localStorage.setItem('last_workspace_id', workspace.id);
|
||||
globalContextService.globalContext.workspaceId.set(workspace.id);
|
||||
if (workspaceServer) {
|
||||
globalContextService.globalContext.serverId.set(workspaceServer.id);
|
||||
}
|
||||
globalContextService.globalContext.workspaceFlavour.set(
|
||||
workspace.flavour
|
||||
);
|
||||
return () => {
|
||||
window.currentWorkspace = undefined;
|
||||
globalContextService.globalContext.workspaceId.set(null);
|
||||
if (workspaceServer) {
|
||||
globalContextService.globalContext.serverId.set(
|
||||
defaultServerService.server.id
|
||||
);
|
||||
}
|
||||
globalContextService.globalContext.workspaceFlavour.set(null);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [globalContextService, workspace]);
|
||||
}, [
|
||||
defaultServerService.server.id,
|
||||
globalContextService,
|
||||
workspace,
|
||||
workspaceServer,
|
||||
]);
|
||||
|
||||
if (!workspace) {
|
||||
return null; // skip this, workspace will be set in layout effect
|
||||
@@ -203,19 +227,23 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
||||
|
||||
if (!isRootDocReady) {
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AppContainer fallback />
|
||||
<FrameworkScope scope={workspaceServer?.scope}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AppContainer fallback />
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<WorkspaceLayout>
|
||||
<WorkbenchRoot />
|
||||
</WorkspaceLayout>
|
||||
</AffineErrorBoundary>
|
||||
<FrameworkScope scope={workspaceServer?.scope}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<WorkspaceLayout>
|
||||
<WorkbenchRoot />
|
||||
</WorkspaceLayout>
|
||||
</AffineErrorBoundary>
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@ import { AppContainer } from '@affine/core/desktop/components/app-container';
|
||||
import { WorkspaceDialogs } from '@affine/core/desktop/dialogs';
|
||||
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
LiveData,
|
||||
useLiveData,
|
||||
@@ -29,10 +28,9 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
||||
<WorkspaceDialogs />
|
||||
|
||||
{/* ---- some side-effect components ---- */}
|
||||
{currentWorkspace?.flavour === WorkspaceFlavour.LOCAL && (
|
||||
{currentWorkspace?.flavour === 'local' ? (
|
||||
<LocalQuotaModal />
|
||||
)}
|
||||
{currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && (
|
||||
) : (
|
||||
<CloudQuotaModal />
|
||||
)}
|
||||
<AiLoginRequiredModal />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AuthModal } from '@affine/core/components/affine/auth';
|
||||
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
|
||||
@@ -30,7 +29,6 @@ export function ShareHeader({
|
||||
snapshotUrl={snapshotUrl}
|
||||
templateName={templateName}
|
||||
/>
|
||||
<AuthModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
||||
import { ShareReaderService } from '@affine/core/modules/share-doc';
|
||||
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
||||
import { CloudBlobStorage } from '@affine/core/modules/workspace-engine';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
type DocMode,
|
||||
@@ -170,7 +169,7 @@ const SharePageInner = ({
|
||||
{
|
||||
metadata: {
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
flavour: 'affine-cloud',
|
||||
},
|
||||
isSharedMode: true,
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,17 @@
|
||||
import { SignInPanel } from '@affine/core/components/sign-in';
|
||||
|
||||
import { MobileSignInLayout } from './layout';
|
||||
|
||||
export const MobileSignInPanel = ({
|
||||
onClose,
|
||||
server,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
server?: string;
|
||||
}) => {
|
||||
return (
|
||||
<MobileSignInLayout>
|
||||
<SignInPanel onClose={onClose} server={server} />
|
||||
</MobileSignInLayout>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { IconButton } from '@affine/component';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { CloseIcon, CollaborationIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
@@ -16,10 +15,8 @@ import { type HTMLAttributes, useCallback, useMemo } from 'react';
|
||||
|
||||
import * as styles from './menu.css';
|
||||
|
||||
const filterByFlavour = (
|
||||
workspaces: WorkspaceMetadata[],
|
||||
flavour: WorkspaceFlavour
|
||||
) => workspaces.filter(ws => flavour === ws.flavour);
|
||||
const filterByFlavour = (workspaces: WorkspaceMetadata[], flavour: string) =>
|
||||
workspaces.filter(ws => flavour === ws.flavour);
|
||||
|
||||
const WorkspaceItem = ({
|
||||
workspace,
|
||||
@@ -93,13 +90,14 @@ export const SelectorMenu = ({ onClose }: { onClose?: () => void }) => {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
|
||||
// TODO: support selfhosted
|
||||
const cloudWorkspaces = useMemo(
|
||||
() => filterByFlavour(workspaces, WorkspaceFlavour.AFFINE_CLOUD),
|
||||
() => filterByFlavour(workspaces, 'affine-cloud'),
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
const localWorkspaces = useMemo(
|
||||
() => filterByFlavour(workspaces, WorkspaceFlavour.LOCAL),
|
||||
() => filterByFlavour(workspaces, 'local'),
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AuthModal } from '@affine/core/components/affine/auth';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
@@ -12,6 +11,7 @@ import { CollectionSelectorDialog } from './selectors/collection-selector';
|
||||
import { DocSelectorDialog } from './selectors/doc-selector';
|
||||
import { TagSelectorDialog } from './selectors/tag-selector';
|
||||
import { SettingDialog } from './setting';
|
||||
import { SignInDialog } from './sign-in';
|
||||
|
||||
const GLOBAL_DIALOGS = {
|
||||
// 'create-workspace': CreateWorkspaceDialog,
|
||||
@@ -19,6 +19,7 @@ const GLOBAL_DIALOGS = {
|
||||
// 'import-template': ImportTemplateDialog,
|
||||
setting: SettingDialog,
|
||||
// import: ImportDialog,
|
||||
'sign-in': SignInDialog,
|
||||
} satisfies {
|
||||
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
||||
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
||||
@@ -58,8 +59,6 @@ export const GlobalDialogs = () => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<AuthModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Avatar } from '@affine/component';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useEnsureLiveData,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { UserPlanTag } from '../../../components';
|
||||
@@ -79,11 +78,11 @@ const AuthorizedUserProfile = () => {
|
||||
};
|
||||
|
||||
const UnauthorizedUserProfile = () => {
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
onClick={() => setAuthModal({ openModal: true, state: 'signIn' })}
|
||||
onClick={() => globalDialogService.open('sign-in', {})}
|
||||
avatar={<Avatar size={48} rounded={4} />}
|
||||
title="Sign up / Sign in"
|
||||
caption="Sync with AFFiNE Cloud"
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
import { IconButton, Modal, SafeArea } from '@affine/component';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { CloseIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { MobileSignIn } from './mobile-sign-in';
|
||||
|
||||
export const MobileSignInModal = () => {
|
||||
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
||||
const setOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
setAuthAtom(prev => ({ ...prev, openModal: open }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
import { MobileSignInPanel } from '../../components/sign-in';
|
||||
|
||||
export const SignInDialog = ({
|
||||
close,
|
||||
server: initialServerBaseUrl,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['sign-in']>) => {
|
||||
return (
|
||||
<Modal
|
||||
fullScreen
|
||||
animation="slideBottom"
|
||||
open={authAtomValue.openModal}
|
||||
onOpenChange={setOpen}
|
||||
open
|
||||
onOpenChange={() => close()}
|
||||
contentOptions={{
|
||||
style: {
|
||||
padding: 0,
|
||||
@@ -35,7 +27,7 @@ export const MobileSignInModal = () => {
|
||||
}}
|
||||
withoutCloseButton
|
||||
>
|
||||
<MobileSignIn onSkip={closeModal} />
|
||||
<MobileSignInPanel onClose={close} server={initialServerBaseUrl} />
|
||||
<SafeArea
|
||||
top
|
||||
style={{ position: 'absolute', top: 0, right: 0, paddingRight: 16 }}
|
||||
@@ -46,7 +38,7 @@ export const MobileSignInModal = () => {
|
||||
variant="solid"
|
||||
icon={<CloseIcon />}
|
||||
style={{ borderRadius: 8, padding: 4 }}
|
||||
onClick={closeModal}
|
||||
onClick={() => close()}
|
||||
/>
|
||||
</SafeArea>
|
||||
</Modal>
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { GlobalDialogs } from '../../dialogs';
|
||||
import { MobileSignInModal } from '../../views/sign-in/modal';
|
||||
|
||||
export const RootWrapper = () => {
|
||||
const defaultServerService = useService(DefaultServerService);
|
||||
@@ -33,7 +32,6 @@ export const RootWrapper = () => {
|
||||
<FrameworkScope scope={defaultServerService.server.scope}>
|
||||
<GlobalDialogs />
|
||||
<NotificationCenter />
|
||||
<MobileSignInModal />
|
||||
<Outlet />
|
||||
</FrameworkScope>
|
||||
);
|
||||
|
||||
@@ -1,38 +1,10 @@
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { MobileSignIn } from '../views/sign-in/mobile-sign-in';
|
||||
import { MobileSignInPanel } from '../components/sign-in';
|
||||
|
||||
export const Component = () => {
|
||||
const session = useService(AuthService).session;
|
||||
const status = useLiveData(session.status$);
|
||||
const isRevalidating = useLiveData(session.isRevalidating$);
|
||||
const navigate = useNavigate();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const [searchParams] = useSearchParams();
|
||||
const isLoggedIn = status === 'authenticated' && !isRevalidating;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
const redirectUri = searchParams.get('redirect_uri');
|
||||
if (redirectUri) {
|
||||
navigate(redirectUri, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE, {
|
||||
search: searchParams.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [jumpToIndex, navigate, isLoggedIn, searchParams]);
|
||||
|
||||
return <MobileSignIn onSkip={() => navigate('/')} />;
|
||||
return <MobileSignInPanel onClose={() => navigate('/')} />;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IconButton, MobileMenu } from '@affine/component';
|
||||
import { SharePage } from '@affine/core/components/affine/share-page-modal/share-menu/share-page';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { ShareiOsIcon } from '@blocksuite/icons/rc';
|
||||
import { DocService, useServices, WorkspaceService } from '@toeverything/infra';
|
||||
|
||||
@@ -16,7 +15,7 @@ export const PageHeaderShareButton = () => {
|
||||
const doc = docService.doc.blockSuiteDoc;
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
if (workspace.meta.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (workspace.meta.flavour === 'local') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,11 @@ import {
|
||||
} from '@affine/core/components/affine/quota-reached-modal';
|
||||
import { SWRConfigProvider } from '@affine/core/components/providers/swr-config-provider';
|
||||
import { WorkspaceSideEffects } from '@affine/core/components/providers/workspace-side-effects';
|
||||
import {
|
||||
DefaultServerService,
|
||||
WorkspaceServerService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
FrameworkScope,
|
||||
@@ -47,12 +50,15 @@ export const WorkspaceLayout = ({
|
||||
children,
|
||||
}: PropsWithChildren<{ meta: WorkspaceMetadata }>) => {
|
||||
// todo: reduce code duplication with packages\frontend\core\src\pages\workspace\index.tsx
|
||||
const { workspacesService, globalContextService } = useServices({
|
||||
WorkspacesService,
|
||||
GlobalContextService,
|
||||
});
|
||||
const { workspacesService, globalContextService, defaultServerService } =
|
||||
useServices({
|
||||
WorkspacesService,
|
||||
GlobalContextService,
|
||||
DefaultServerService,
|
||||
});
|
||||
|
||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||
const workspaceServer = workspace?.scope.get(WorkspaceServerService)?.server;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ref = workspacesService.open({ metadata: meta });
|
||||
@@ -75,13 +81,30 @@ export const WorkspaceLayout = ({
|
||||
);
|
||||
localStorage.setItem('last_workspace_id', workspace.id);
|
||||
globalContextService.globalContext.workspaceId.set(workspace.id);
|
||||
if (workspaceServer) {
|
||||
globalContextService.globalContext.serverId.set(workspaceServer.id);
|
||||
}
|
||||
globalContextService.globalContext.workspaceFlavour.set(
|
||||
workspace.flavour
|
||||
);
|
||||
return () => {
|
||||
window.currentWorkspace = undefined;
|
||||
globalContextService.globalContext.workspaceId.set(null);
|
||||
if (workspaceServer) {
|
||||
globalContextService.globalContext.serverId.set(
|
||||
defaultServerService.server.id
|
||||
);
|
||||
}
|
||||
globalContextService.globalContext.workspaceFlavour.set(null);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [globalContextService, workspace]);
|
||||
}, [
|
||||
defaultServerService.server.id,
|
||||
globalContextService,
|
||||
workspace,
|
||||
workspaceServer,
|
||||
]);
|
||||
|
||||
const isRootDocReady =
|
||||
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
|
||||
@@ -95,22 +118,25 @@ export const WorkspaceLayout = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AffineErrorBoundary height="100dvh">
|
||||
<SWRConfigProvider>
|
||||
<WorkspaceDialogs />
|
||||
<FrameworkScope scope={workspaceServer?.scope}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AffineErrorBoundary height="100dvh">
|
||||
<SWRConfigProvider>
|
||||
<WorkspaceDialogs />
|
||||
|
||||
{/* ---- some side-effect components ---- */}
|
||||
<PeekViewManagerModal />
|
||||
{workspace?.flavour === WorkspaceFlavour.LOCAL && <LocalQuotaModal />}
|
||||
{workspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && (
|
||||
<CloudQuotaModal />
|
||||
)}
|
||||
<AiLoginRequiredModal />
|
||||
<WorkspaceSideEffects />
|
||||
{children}
|
||||
</SWRConfigProvider>
|
||||
</AffineErrorBoundary>
|
||||
{/* ---- some side-effect components ---- */}
|
||||
<PeekViewManagerModal />
|
||||
{workspace?.flavour === 'local' ? (
|
||||
<LocalQuotaModal />
|
||||
) : (
|
||||
<CloudQuotaModal />
|
||||
)}
|
||||
<AiLoginRequiredModal />
|
||||
<WorkspaceSideEffects />
|
||||
{children}
|
||||
</SWRConfigProvider>
|
||||
</AffineErrorBoundary>
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { AuthPanel } from '@affine/core/components/affine/auth';
|
||||
|
||||
import { MobileSignInLayout } from './layout';
|
||||
|
||||
export const MobileSignIn = ({ onSkip }: { onSkip: () => void }) => {
|
||||
return (
|
||||
<MobileSignInLayout>
|
||||
<AuthPanel onSkip={onSkip} />
|
||||
</MobileSignInLayout>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { EMPTY, exhaustMap, map, mergeMap } from 'rxjs';
|
||||
|
||||
import { ServerScope } from '../scopes/server';
|
||||
import { AuthService } from '../services/auth';
|
||||
import { FetchService } from '../services/fetch';
|
||||
import { GraphQLService } from '../services/graphql';
|
||||
import { ServerConfigStore } from '../stores/server-config';
|
||||
@@ -34,6 +35,9 @@ export class Server extends Entity<{
|
||||
readonly serverConfigStore = this.scope.framework.get(ServerConfigStore);
|
||||
readonly fetch = this.scope.framework.get(FetchService).fetch;
|
||||
readonly gql = this.scope.framework.get(GraphQLService).gql;
|
||||
get account$() {
|
||||
return this.scope.framework.get(AuthService).session.account$;
|
||||
}
|
||||
readonly serverMetadata = this.props.serverMetadata;
|
||||
|
||||
constructor(private readonly serverListStore: ServerListStore) {
|
||||
@@ -68,7 +72,7 @@ export class Server extends Entity<{
|
||||
readonly revalidateConfig = effect(
|
||||
exhaustMap(() => {
|
||||
return fromPromise(signal =>
|
||||
this.serverConfigStore.fetchServerConfig(signal)
|
||||
this.serverConfigStore.fetchServerConfig(this.baseUrl, signal)
|
||||
).pipe(
|
||||
backoffRetry({
|
||||
count: Infinity,
|
||||
|
||||
@@ -68,7 +68,7 @@ export class AuthSession extends Entity {
|
||||
|
||||
revalidate = effect(
|
||||
exhaustMapWithTrailing(() =>
|
||||
fromPromise(this.getSession()).pipe(
|
||||
fromPromise(() => this.getSession()).pipe(
|
||||
backoffRetry({
|
||||
count: Infinity,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createEvent } from '@toeverything/infra';
|
||||
|
||||
import type { AuthAccountInfo } from '../entities/session';
|
||||
|
||||
export const AccountChanged = createEvent<AuthAccountInfo | null>(
|
||||
'AccountChanged'
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createEvent } from '@toeverything/infra';
|
||||
|
||||
import type { AuthAccountInfo } from '../entities/session';
|
||||
|
||||
export const AccountLoggedIn = createEvent<AuthAccountInfo>('AccountLoggedIn');
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createEvent } from '@toeverything/infra';
|
||||
|
||||
import type { AuthAccountInfo } from '../entities/session';
|
||||
|
||||
export const AccountLoggedOut =
|
||||
createEvent<AuthAccountInfo>('AccountLoggedOut');
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createEvent } from '@toeverything/infra';
|
||||
|
||||
import type { Server } from '../entities/server';
|
||||
|
||||
export const ServerInitialized = createEvent<Server>('ServerInitialized');
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createEvent } from '@toeverything/infra';
|
||||
|
||||
export const ServerStarted = createEvent('ServerStarted');
|
||||
@@ -7,10 +7,14 @@ export {
|
||||
isNetworkError,
|
||||
NetworkError,
|
||||
} from './error';
|
||||
export { AccountChanged } from './events/account-changed';
|
||||
export { AccountLoggedIn } from './events/account-logged-in';
|
||||
export { AccountLoggedOut } from './events/account-logged-out';
|
||||
export { ServerInitialized } from './events/server-initialized';
|
||||
export { RawFetchProvider } from './provider/fetch';
|
||||
export { ValidatorProvider } from './provider/validator';
|
||||
export { WebSocketAuthProvider } from './provider/websocket-auth';
|
||||
export { AccountChanged, AuthService } from './services/auth';
|
||||
export { AuthService } from './services/auth';
|
||||
export { CaptchaService } from './services/captcha';
|
||||
export { DefaultServerService } from './services/default-server';
|
||||
export { EventSourceService } from './services/eventsource';
|
||||
@@ -25,6 +29,7 @@ export { UserFeatureService } from './services/user-feature';
|
||||
export { UserQuotaService } from './services/user-quota';
|
||||
export { WebSocketService } from './services/websocket';
|
||||
export { WorkspaceServerService } from './services/workspace-server';
|
||||
export type { ServerConfig } from './types';
|
||||
|
||||
import {
|
||||
DocScope,
|
||||
@@ -79,9 +84,10 @@ import { UserQuotaStore } from './stores/user-quota';
|
||||
export function configureCloudModule(framework: Framework) {
|
||||
framework
|
||||
.impl(RawFetchProvider, DefaultRawFetchProvider)
|
||||
.service(ServersService, [ServerListStore])
|
||||
.service(ServersService, [ServerListStore, ServerConfigStore])
|
||||
.service(DefaultServerService, [ServersService])
|
||||
.store(ServerListStore, [GlobalStateService])
|
||||
.store(ServerConfigStore, [RawFetchProvider])
|
||||
.entity(Server, [ServerListStore])
|
||||
.scope(ServerScope)
|
||||
.service(ServerService, [ServerScope])
|
||||
@@ -97,7 +103,6 @@ export function configureCloudModule(framework: Framework) {
|
||||
f.getOptional(WebSocketAuthProvider)
|
||||
)
|
||||
)
|
||||
.store(ServerConfigStore, [GraphQLService])
|
||||
.service(CaptchaService, f => {
|
||||
return new CaptchaService(
|
||||
f.get(ServerService),
|
||||
@@ -106,7 +111,12 @@ export function configureCloudModule(framework: Framework) {
|
||||
);
|
||||
})
|
||||
.service(AuthService, [FetchService, AuthStore, UrlService])
|
||||
.store(AuthStore, [FetchService, GraphQLService, GlobalState])
|
||||
.store(AuthStore, [
|
||||
FetchService,
|
||||
GraphQLService,
|
||||
GlobalState,
|
||||
ServerService,
|
||||
])
|
||||
.entity(AuthSession, [AuthStore])
|
||||
.service(SubscriptionService, [SubscriptionStore])
|
||||
.store(SubscriptionStore, [
|
||||
@@ -132,12 +142,13 @@ export function configureCloudModule(framework: Framework) {
|
||||
.store(UserFeatureStore, [GraphQLService])
|
||||
.service(InvoicesService)
|
||||
.store(InvoicesStore, [GraphQLService])
|
||||
.entity(Invoices, [InvoicesStore])
|
||||
.entity(Invoices, [InvoicesStore]);
|
||||
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(WorkspaceServerService)
|
||||
.scope(DocScope)
|
||||
.service(CloudDocMetaService)
|
||||
.entity(CloudDocMeta, [CloudDocMetaStore, DocService, GlobalCache])
|
||||
.store(CloudDocMetaStore, [GraphQLService]);
|
||||
|
||||
framework.scope(WorkspaceScope).service(WorkspaceServerService);
|
||||
.store(CloudDocMetaStore, [WorkspaceServerService]);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||
import type { OAuthProviderType } from '@affine/graphql';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
ApplicationFocused,
|
||||
ApplicationStarted,
|
||||
createEvent,
|
||||
OnEvent,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
import { ApplicationFocused, OnEvent, Service } from '@toeverything/infra';
|
||||
import { distinctUntilChanged, map, skip } from 'rxjs';
|
||||
|
||||
import type { UrlService } from '../../url';
|
||||
import { type AuthAccountInfo, AuthSession } from '../entities/session';
|
||||
import { BackendError } from '../error';
|
||||
import { AccountChanged } from '../events/account-changed';
|
||||
import { AccountLoggedIn } from '../events/account-logged-in';
|
||||
import { AccountLoggedOut } from '../events/account-logged-out';
|
||||
import { ServerStarted } from '../events/server-started';
|
||||
import type { AuthStore } from '../stores/auth';
|
||||
import type { FetchService } from './fetch';
|
||||
|
||||
@@ -26,18 +24,8 @@ function toAIUserInfo(account: AuthAccountInfo | null) {
|
||||
};
|
||||
}
|
||||
|
||||
// Emit when account changed
|
||||
export const AccountChanged = createEvent<AuthAccountInfo | null>(
|
||||
'AccountChanged'
|
||||
);
|
||||
|
||||
export const AccountLoggedIn = createEvent<AuthAccountInfo>('AccountLoggedIn');
|
||||
|
||||
export const AccountLoggedOut =
|
||||
createEvent<AuthAccountInfo>('AccountLoggedOut');
|
||||
|
||||
@OnEvent(ApplicationStarted, e => e.onApplicationStart)
|
||||
@OnEvent(ApplicationFocused, e => e.onApplicationFocused)
|
||||
@OnEvent(ServerStarted, e => e.onServerStarted)
|
||||
export class AuthService extends Service {
|
||||
session = this.framework.createEntity(AuthSession);
|
||||
|
||||
@@ -74,7 +62,7 @@ export class AuthService extends Service {
|
||||
});
|
||||
}
|
||||
|
||||
private onApplicationStart() {
|
||||
private onServerStarted() {
|
||||
this.session.revalidate();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
onStart,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
import { EMPTY, exhaustMap, mergeMap, switchMap } from 'rxjs';
|
||||
|
||||
import type { ValidatorProvider } from '../provider/validator';
|
||||
import type { FetchService } from './fetch';
|
||||
@@ -61,10 +61,12 @@ export class CaptchaService extends Service {
|
||||
mergeMap(({ challenge, token }) => {
|
||||
this.verifyToken$.next(token);
|
||||
this.challenge$.next(challenge);
|
||||
this.resetAfter5min();
|
||||
return EMPTY;
|
||||
}),
|
||||
catchErrorInto(this.error$),
|
||||
onStart(() => {
|
||||
this.challenge$.next(undefined);
|
||||
this.verifyToken$.next(undefined);
|
||||
this.isLoading$.next(true);
|
||||
}),
|
||||
@@ -72,4 +74,22 @@ export class CaptchaService extends Service {
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
resetAfter5min = effect(
|
||||
switchMap(() => {
|
||||
return fromPromise(async () => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 1000 * 60 * 5);
|
||||
});
|
||||
return true;
|
||||
}).pipe(
|
||||
mergeMap(_ => {
|
||||
this.challenge$.next(undefined);
|
||||
this.verifyToken$.next(undefined);
|
||||
this.isLoading$.next(false);
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { LiveData, ObjectPool, Service } from '@toeverything/infra';
|
||||
import { finalize, of, switchMap } from 'rxjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Observable, switchMap } from 'rxjs';
|
||||
|
||||
import { Server } from '../entities/server';
|
||||
import { ServerInitialized } from '../events/server-initialized';
|
||||
import { ServerStarted } from '../events/server-started';
|
||||
import type { ServerConfigStore } from '../stores/server-config';
|
||||
import type { ServerListStore } from '../stores/server-list';
|
||||
import type { ServerConfig, ServerMetadata } from '../types';
|
||||
|
||||
export class ServersService extends Service {
|
||||
constructor(private readonly serverListStore: ServerListStore) {
|
||||
constructor(
|
||||
private readonly serverListStore: ServerListStore,
|
||||
private readonly serverConfigStore: ServerConfigStore
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -21,17 +29,21 @@ export class ServersService extends Service {
|
||||
const server = this.framework.createEntity(Server, {
|
||||
serverMetadata: metadata,
|
||||
});
|
||||
server.revalidateConfig();
|
||||
this.eventBus.emit(ServerInitialized, server);
|
||||
server.scope.eventBus.emit(ServerStarted, server);
|
||||
const ref = this.serverPool.put(metadata.id, server);
|
||||
return ref;
|
||||
});
|
||||
|
||||
return of(refs.map(ref => ref.obj)).pipe(
|
||||
finalize(() => {
|
||||
return new Observable<Server[]>(subscribe => {
|
||||
subscribe.next(refs.map(ref => ref.obj));
|
||||
return () => {
|
||||
refs.forEach(ref => {
|
||||
ref.release();
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
});
|
||||
})
|
||||
),
|
||||
[] as any
|
||||
@@ -52,4 +64,43 @@ export class ServersService extends Service {
|
||||
addServer(metadata: ServerMetadata, config: ServerConfig) {
|
||||
this.serverListStore.addServer(metadata, config);
|
||||
}
|
||||
|
||||
removeServer(id: string) {
|
||||
this.serverListStore.removeServer(id);
|
||||
}
|
||||
|
||||
async addServerByBaseUrl(baseUrl: string) {
|
||||
const config = await this.serverConfigStore.fetchServerConfig(baseUrl);
|
||||
const id = nanoid();
|
||||
this.serverListStore.addServer(
|
||||
{ id, baseUrl },
|
||||
{
|
||||
credentialsRequirement: config.credentialsRequirement,
|
||||
features: config.features,
|
||||
oauthProviders: config.oauthProviders,
|
||||
serverName: config.name,
|
||||
type: config.type,
|
||||
initialized: config.initialized,
|
||||
version: config.version,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getServerByBaseUrl(baseUrl: string) {
|
||||
return this.servers$.value.find(s => s.baseUrl === baseUrl);
|
||||
}
|
||||
|
||||
async addOrGetServerByBaseUrl(baseUrl: string) {
|
||||
const server = this.getServerByBaseUrl(baseUrl);
|
||||
if (server) {
|
||||
return server;
|
||||
} else {
|
||||
await this.addServerByBaseUrl(baseUrl);
|
||||
const server = this.getServerByBaseUrl(baseUrl);
|
||||
if (!server) {
|
||||
throw new Unreachable();
|
||||
}
|
||||
return server;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { OnEvent, Service } from '@toeverything/infra';
|
||||
|
||||
import { Subscription } from '../entities/subscription';
|
||||
import { SubscriptionPrices } from '../entities/subscription-prices';
|
||||
import { AccountChanged } from '../events/account-changed';
|
||||
import type { SubscriptionStore } from '../stores/subscription';
|
||||
import { AccountChanged } from './auth';
|
||||
|
||||
@OnEvent(AccountChanged, e => e.onAccountChanged)
|
||||
export class SubscriptionService extends Service {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OnEvent, Service } from '@toeverything/infra';
|
||||
|
||||
import { UserCopilotQuota } from '../entities/user-copilot-quota';
|
||||
import { AccountChanged } from './auth';
|
||||
import { AccountChanged } from '../events/account-changed';
|
||||
|
||||
@OnEvent(AccountChanged, e => e.onAccountChanged)
|
||||
export class UserCopilotQuotaService extends Service {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OnEvent, Service } from '@toeverything/infra';
|
||||
|
||||
import { UserFeature } from '../entities/user-feature';
|
||||
import { AccountChanged } from './auth';
|
||||
import { AccountChanged } from '../events/account-changed';
|
||||
|
||||
@OnEvent(AccountChanged, e => e.onAccountChanged)
|
||||
export class UserFeatureService extends Service {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mixpanel } from '@affine/track';
|
||||
import { OnEvent, Service } from '@toeverything/infra';
|
||||
|
||||
import { UserQuota } from '../entities/user-quota';
|
||||
import { AccountChanged } from './auth';
|
||||
import { AccountChanged } from '../events/account-changed';
|
||||
|
||||
@OnEvent(AccountChanged, e => e.onAccountChanged)
|
||||
export class UserQuotaService extends Service {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
|
||||
import { Manager } from 'socket.io-client';
|
||||
|
||||
import { AccountChanged } from '../events/account-changed';
|
||||
import type { WebSocketAuthProvider } from '../provider/websocket-auth';
|
||||
import type { AuthService } from './auth';
|
||||
import { AccountChanged } from './auth';
|
||||
import type { ServerService } from './server';
|
||||
|
||||
@OnEvent(AccountChanged, e => e.update)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Store } from '@toeverything/infra';
|
||||
import type { AuthSessionInfo } from '../entities/session';
|
||||
import type { FetchService } from '../services/fetch';
|
||||
import type { GraphQLService } from '../services/graphql';
|
||||
import type { ServerService } from '../services/server';
|
||||
|
||||
export interface AccountProfile {
|
||||
id: string;
|
||||
@@ -23,21 +24,26 @@ export class AuthStore extends Store {
|
||||
constructor(
|
||||
private readonly fetchService: FetchService,
|
||||
private readonly gqlService: GraphQLService,
|
||||
private readonly globalState: GlobalState
|
||||
private readonly globalState: GlobalState,
|
||||
private readonly serverService: ServerService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
watchCachedAuthSession() {
|
||||
return this.globalState.watch<AuthSessionInfo>('affine-cloud-auth');
|
||||
return this.globalState.watch<AuthSessionInfo>(
|
||||
`${this.serverService.server.id}-auth`
|
||||
);
|
||||
}
|
||||
|
||||
getCachedAuthSession() {
|
||||
return this.globalState.get<AuthSessionInfo>('affine-cloud-auth');
|
||||
return this.globalState.get<AuthSessionInfo>(
|
||||
`${this.serverService.server.id}-auth`
|
||||
);
|
||||
}
|
||||
|
||||
setCachedAuthSession(session: AuthSessionInfo | null) {
|
||||
this.globalState.set('affine-cloud-auth', session);
|
||||
this.globalState.set(`${this.serverService.server.id}-auth`, session);
|
||||
}
|
||||
|
||||
async fetchSession() {
|
||||
@@ -99,6 +105,7 @@ export class AuthStore extends Store {
|
||||
const data = (await res.json()) as {
|
||||
registered: boolean;
|
||||
hasPassword: boolean;
|
||||
magicLink: boolean;
|
||||
};
|
||||
|
||||
return data;
|
||||
|
||||
@@ -2,10 +2,10 @@ import { getWorkspacePageMetaByIdQuery } from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import { type CloudDocMetaType } from '../entities/cloud-doc-meta';
|
||||
import type { GraphQLService } from '../services/graphql';
|
||||
import type { WorkspaceServerService } from '../services/workspace-server';
|
||||
|
||||
export class CloudDocMetaStore extends Store {
|
||||
constructor(private readonly gqlService: GraphQLService) {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ export class CloudDocMetaStore extends Store {
|
||||
docId: string,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<CloudDocMetaType> {
|
||||
const serverConfigData = await this.gqlService.gql({
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('Server not found');
|
||||
}
|
||||
const serverConfigData = await this.workspaceServerService.server.gql({
|
||||
query: getWorkspacePageMetaByIdQuery,
|
||||
variables: { id: workspaceId, pageId: docId },
|
||||
context: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
gqlFetcherFactory,
|
||||
type OauthProvidersQuery,
|
||||
oauthProvidersQuery,
|
||||
type ServerConfigQuery,
|
||||
@@ -7,27 +8,32 @@ import {
|
||||
} from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { GraphQLService } from '../services/graphql';
|
||||
import type { RawFetchProvider } from '../provider/fetch';
|
||||
|
||||
export type ServerConfigType = ServerConfigQuery['serverConfig'] &
|
||||
OauthProvidersQuery['serverConfig'];
|
||||
|
||||
export class ServerConfigStore extends Store {
|
||||
constructor(private readonly gqlService: GraphQLService) {
|
||||
constructor(private readonly fetcher: RawFetchProvider) {
|
||||
super();
|
||||
}
|
||||
|
||||
async fetchServerConfig(
|
||||
serverBaseUrl: string,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<ServerConfigType> {
|
||||
const serverConfigData = await this.gqlService.gql({
|
||||
const gql = gqlFetcherFactory(
|
||||
`${serverBaseUrl}/graphql`,
|
||||
this.fetcher.fetch
|
||||
);
|
||||
const serverConfigData = await gql({
|
||||
query: serverConfigQuery,
|
||||
context: {
|
||||
signal: abortSignal,
|
||||
},
|
||||
});
|
||||
if (serverConfigData.serverConfig.features.includes(ServerFeature.OAuth)) {
|
||||
const oauthProvidersData = await this.gqlService.gql({
|
||||
const oauthProvidersData = await gql({
|
||||
query: oauthProvidersQuery,
|
||||
context: {
|
||||
signal: abortSignal,
|
||||
|
||||
@@ -31,11 +31,17 @@ export class ServerListStore extends Store {
|
||||
}
|
||||
|
||||
addServer(server: ServerMetadata, serverConfig: ServerConfig) {
|
||||
this.updateServerConfig(server.id, serverConfig);
|
||||
const oldServers =
|
||||
this.globalStateService.globalState.get<ServerMetadata[]>('serverList') ??
|
||||
[];
|
||||
|
||||
if (oldServers.some(s => s.baseUrl === server.baseUrl)) {
|
||||
throw new Error(
|
||||
'Server with same base url already exists, ' + server.baseUrl
|
||||
);
|
||||
}
|
||||
|
||||
this.updateServerConfig(server.id, serverConfig);
|
||||
this.globalStateService.globalState.set<ServerMetadata[]>('serverList', [
|
||||
...oldServers,
|
||||
server,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useNavigationType,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { AuthService, ServersService } from '../../cloud';
|
||||
import { AuthService, DefaultServerService, ServersService } from '../../cloud';
|
||||
import type { DesktopApi } from '../entities/electron-api';
|
||||
|
||||
@OnEvent(ApplicationStarted, e => e.setupStartListener)
|
||||
@@ -138,20 +138,26 @@ export class DesktopApiService extends Service {
|
||||
}
|
||||
|
||||
private setupAuthRequestEvent() {
|
||||
this.events.ui.onAuthenticationRequest(({ method, payload }) => {
|
||||
this.events.ui.onAuthenticationRequest(({ method, payload, server }) => {
|
||||
(async () => {
|
||||
if (!(await this.api.handler.ui.isActiveTab())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: support multiple servers
|
||||
const affineCloudServer = this.framework
|
||||
.get(ServersService)
|
||||
.server$('affine-cloud').value;
|
||||
if (!affineCloudServer) {
|
||||
// Dynamically get these services to avoid circular dependencies
|
||||
const serversService = this.framework.get(ServersService);
|
||||
const defaultServerService = this.framework.get(DefaultServerService);
|
||||
|
||||
let targetServer;
|
||||
if (server) {
|
||||
targetServer = await serversService.addOrGetServerByBaseUrl(server);
|
||||
} else {
|
||||
targetServer = defaultServerService.server;
|
||||
}
|
||||
if (!targetServer) {
|
||||
throw new Error('Affine Cloud server not found');
|
||||
}
|
||||
const authService = affineCloudServer.scope.get(AuthService);
|
||||
const authService = targetServer.scope.get(AuthService);
|
||||
|
||||
switch (method) {
|
||||
case 'magic-link': {
|
||||
|
||||
@@ -31,6 +31,9 @@ export type GLOBAL_DIALOG_SCHEMA = {
|
||||
workspaceMetadata?: WorkspaceMetadata | null;
|
||||
scrollAnchor?: string;
|
||||
}) => void;
|
||||
'sign-in': (props: { server?: string; step?: 'sign-in' }) => void;
|
||||
'change-password': (props: { server?: string }) => void;
|
||||
'verify-email': (props: { server?: string; changeEmail?: boolean }) => void;
|
||||
};
|
||||
|
||||
export type WORKSPACE_DIALOG_SCHEMA = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { WorkspaceDBService, WorkspaceService } from '@toeverything/infra';
|
||||
import { LiveData, Store } from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
@@ -27,7 +26,7 @@ export class FavoriteStore extends Store {
|
||||
// if is local workspace or no account, use __local__ userdata
|
||||
// sometimes we may have cloud workspace but no account for a short time, we also use __local__ userdata
|
||||
if (
|
||||
this.workspaceService.workspace.meta.flavour === WorkspaceFlavour.LOCAL ||
|
||||
this.workspaceService.workspace.meta.flavour === 'local' ||
|
||||
!this.authService
|
||||
) {
|
||||
return new LiveData(this.workspaceDBService.userdataDB('__local__'));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks';
|
||||
import type { WorkspaceMetadata, WorkspacesService } from '@toeverything/infra';
|
||||
import { DocsService, Service } from '@toeverything/infra';
|
||||
@@ -36,7 +35,7 @@ export class ImportTemplateService extends Service {
|
||||
}
|
||||
|
||||
async importToNewWorkspace(
|
||||
flavour: WorkspaceFlavour,
|
||||
flavour: string,
|
||||
workspaceName: string,
|
||||
docBinary: Uint8Array
|
||||
// todo: support doc mode on init
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { WorkspaceService } from '@toeverything/infra';
|
||||
import {
|
||||
backoffRetry,
|
||||
@@ -34,10 +33,7 @@ export class WorkspacePermission extends Entity {
|
||||
revalidate = effect(
|
||||
exhaustMap(() => {
|
||||
return fromPromise(async signal => {
|
||||
if (
|
||||
this.workspaceService.workspace.flavour ===
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
) {
|
||||
if (this.workspaceService.workspace.flavour !== 'local') {
|
||||
return await this.store.fetchIsOwner(
|
||||
this.workspaceService.workspace.id,
|
||||
signal
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { WorkspaceService } from '@toeverything/infra';
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
@@ -10,7 +9,7 @@ export class ShareDocsListService extends Service {
|
||||
}
|
||||
|
||||
shareDocs =
|
||||
this.workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
this.workspaceService.workspace.flavour !== 'local'
|
||||
? this.framework.createEntity(ShareDocsList)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type Framework, GlobalContextService } from '@toeverything/infra';
|
||||
|
||||
import { ServersService } from '../cloud/services/servers';
|
||||
import { ServersService } from '../cloud';
|
||||
import { TelemetryService } from './services/telemetry';
|
||||
|
||||
export function configureTelemetryModule(framework: Framework) {
|
||||
framework.service(TelemetryService, [ServersService, GlobalContextService]);
|
||||
framework.service(TelemetryService, [GlobalContextService, ServersService]);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,58 @@
|
||||
import { mixpanel } from '@affine/track';
|
||||
import type { GlobalContextService } from '@toeverything/infra';
|
||||
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
|
||||
import {
|
||||
ApplicationStarted,
|
||||
LiveData,
|
||||
OnEvent,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { AccountChanged, type AuthAccountInfo, AuthService } from '../../cloud';
|
||||
import { AccountLoggedOut } from '../../cloud/services/auth';
|
||||
import type { ServersService } from '../../cloud/services/servers';
|
||||
import type { AuthAccountInfo, Server, ServersService } from '../../cloud';
|
||||
|
||||
@OnEvent(ApplicationStarted, e => e.onApplicationStart)
|
||||
@OnEvent(AccountChanged, e => e.updateIdentity)
|
||||
@OnEvent(AccountLoggedOut, e => e.onAccountLoggedOut)
|
||||
export class TelemetryService extends Service {
|
||||
private readonly authService;
|
||||
private readonly disposableFns: (() => void)[] = [];
|
||||
|
||||
private readonly currentAccount$ =
|
||||
this.globalContextService.globalContext.serverId.$.selector(id =>
|
||||
id
|
||||
? this.serversService.server$(id)
|
||||
: new LiveData<Server | undefined>(undefined)
|
||||
)
|
||||
.flat()
|
||||
.selector(server => server?.account$)
|
||||
.flat();
|
||||
|
||||
constructor(
|
||||
serversService: ServersService,
|
||||
private readonly globalContextService: GlobalContextService
|
||||
private readonly globalContextService: GlobalContextService,
|
||||
private readonly serversService: ServersService
|
||||
) {
|
||||
super();
|
||||
|
||||
// TODO: support multiple servers
|
||||
const affineCloudServer = serversService.server$('affine-cloud').value;
|
||||
if (!affineCloudServer) {
|
||||
throw new Error('affine-cloud server not found');
|
||||
}
|
||||
this.authService = affineCloudServer.scope.get(AuthService);
|
||||
}
|
||||
|
||||
onApplicationStart() {
|
||||
const account = this.authService.session.account$.value;
|
||||
this.updateIdentity(account);
|
||||
this.registerMiddlewares();
|
||||
}
|
||||
|
||||
updateIdentity(account: AuthAccountInfo | null) {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
mixpanel.identify(account.id);
|
||||
mixpanel.people.set({
|
||||
$email: account.email,
|
||||
$name: account.label,
|
||||
$avatar: account.avatar,
|
||||
let prevAccount: AuthAccountInfo | null = null;
|
||||
const unsubscribe = this.currentAccount$.subscribe(account => {
|
||||
if (prevAccount) {
|
||||
mixpanel.reset();
|
||||
}
|
||||
prevAccount = account ?? null;
|
||||
if (account) {
|
||||
mixpanel.identify(account.id);
|
||||
mixpanel.people.set({
|
||||
$email: account.email,
|
||||
$name: account.label,
|
||||
$avatar: account.avatar,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.disposableFns.push(() => {
|
||||
unsubscribe.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
onAccountLoggedOut() {
|
||||
mixpanel.reset();
|
||||
onApplicationStart() {
|
||||
this.registerMiddlewares();
|
||||
}
|
||||
|
||||
registerMiddlewares() {
|
||||
@@ -59,7 +67,7 @@ export class TelemetryService extends Service {
|
||||
);
|
||||
}
|
||||
|
||||
extractGlobalContext(): { page?: string } {
|
||||
extractGlobalContext(): { page?: string; serverId?: string } {
|
||||
const globalContext = this.globalContextService.globalContext;
|
||||
const page = globalContext.isDoc.get()
|
||||
? globalContext.isTrashDoc.get()
|
||||
@@ -76,11 +84,12 @@ export class TelemetryService extends Service {
|
||||
: globalContext.isTag.get()
|
||||
? 'tag'
|
||||
: undefined;
|
||||
return { page };
|
||||
const serverId = globalContext.serverId.get() ?? undefined;
|
||||
return { page, serverId };
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.disposables.forEach(dispose => dispose());
|
||||
this.disposableFns.forEach(dispose => dispose());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
createWorkspaceMutation,
|
||||
deleteWorkspaceMutation,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
} from '@affine/graphql';
|
||||
import { DocCollection } from '@blocksuite/affine/store';
|
||||
import {
|
||||
ApplicationStarted,
|
||||
type BlobStorage,
|
||||
catchErrorInto,
|
||||
type DocStorage,
|
||||
@@ -16,22 +14,24 @@ import {
|
||||
fromPromise,
|
||||
type GlobalState,
|
||||
LiveData,
|
||||
ObjectPool,
|
||||
onComplete,
|
||||
OnEvent,
|
||||
onStart,
|
||||
Service,
|
||||
type Workspace,
|
||||
type WorkspaceEngineProvider,
|
||||
type WorkspaceFlavourProvider,
|
||||
type WorkspaceFlavoursProvider,
|
||||
type WorkspaceMetadata,
|
||||
type WorkspaceProfileInfo,
|
||||
} from '@toeverything/infra';
|
||||
import { effect, getAFFiNEWorkspaceSchema, Service } from '@toeverything/infra';
|
||||
import { effect, getAFFiNEWorkspaceSchema } from '@toeverything/infra';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { EMPTY, map, mergeMap } from 'rxjs';
|
||||
import { EMPTY, map, mergeMap, Observable, switchMap } from 'rxjs';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import type { Server } from '../../cloud';
|
||||
import type { Server, ServersService } from '../../cloud';
|
||||
import {
|
||||
AccountChanged,
|
||||
AuthService,
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
WebSocketService,
|
||||
WorkspaceServerService,
|
||||
} from '../../cloud';
|
||||
import type { ServersService } from '../../cloud/services/servers';
|
||||
import type { WorkspaceEngineStorageProvider } from '../providers/engine';
|
||||
import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel';
|
||||
import { CloudAwarenessConnection } from './engine/awareness-cloud';
|
||||
@@ -49,41 +48,41 @@ import { StaticBlobStorage } from './engine/blob-static';
|
||||
import { CloudDocEngineServer } from './engine/doc-cloud';
|
||||
import { CloudStaticDocStorage } from './engine/doc-cloud-static';
|
||||
|
||||
const CLOUD_WORKSPACES_CACHE_KEY = 'cloud-workspace:';
|
||||
const getCloudWorkspaceCacheKey = (serverId: string) => {
|
||||
if (serverId === 'affine-cloud') {
|
||||
return 'cloud-workspace:'; // FOR BACKWARD COMPATIBILITY
|
||||
}
|
||||
return `selfhosted-workspace-${serverId}:`;
|
||||
};
|
||||
|
||||
const logger = new DebugLogger('affine:cloud-workspace-flavour-provider');
|
||||
|
||||
@OnEvent(ApplicationStarted, e => e.revalidate)
|
||||
@OnEvent(AccountChanged, e => e.revalidate)
|
||||
export class CloudWorkspaceFlavourProviderService
|
||||
extends Service
|
||||
implements WorkspaceFlavourProvider
|
||||
{
|
||||
class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
private readonly authService: AuthService;
|
||||
private readonly webSocketService: WebSocketService;
|
||||
private readonly fetchService: FetchService;
|
||||
private readonly graphqlService: GraphQLService;
|
||||
private readonly affineCloudServer: Server;
|
||||
|
||||
private readonly unsubscribeAccountChanged: () => void;
|
||||
|
||||
constructor(
|
||||
private readonly globalState: GlobalState,
|
||||
private readonly storageProvider: WorkspaceEngineStorageProvider,
|
||||
serversService: ServersService
|
||||
private readonly server: Server
|
||||
) {
|
||||
super();
|
||||
// TODO: support multiple servers
|
||||
const affineCloudServer = serversService.server$('affine-cloud').value;
|
||||
if (!affineCloudServer) {
|
||||
throw new Error('affine-cloud server not found');
|
||||
}
|
||||
this.affineCloudServer = affineCloudServer;
|
||||
this.authService = affineCloudServer.scope.get(AuthService);
|
||||
this.webSocketService = affineCloudServer.scope.get(WebSocketService);
|
||||
this.fetchService = affineCloudServer.scope.get(FetchService);
|
||||
this.graphqlService = affineCloudServer.scope.get(GraphQLService);
|
||||
this.authService = server.scope.get(AuthService);
|
||||
this.webSocketService = server.scope.get(WebSocketService);
|
||||
this.fetchService = server.scope.get(FetchService);
|
||||
this.graphqlService = server.scope.get(GraphQLService);
|
||||
this.unsubscribeAccountChanged = this.server.scope.eventBus.on(
|
||||
AccountChanged,
|
||||
() => {
|
||||
this.revalidate();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
flavour: WorkspaceFlavour = WorkspaceFlavour.AFFINE_CLOUD;
|
||||
flavour = this.server.id;
|
||||
|
||||
async deleteWorkspace(id: string): Promise<void> {
|
||||
await this.graphqlService.gql({
|
||||
@@ -95,6 +94,7 @@ export class CloudWorkspaceFlavourProviderService
|
||||
this.revalidate();
|
||||
await this.waitForLoaded();
|
||||
}
|
||||
|
||||
async createWorkspace(
|
||||
initial: (
|
||||
docCollection: DocCollection,
|
||||
@@ -139,7 +139,7 @@ export class CloudWorkspaceFlavourProviderService
|
||||
|
||||
return {
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
flavour: this.server.id,
|
||||
};
|
||||
}
|
||||
revalidate = effect(
|
||||
@@ -169,7 +169,7 @@ export class CloudWorkspaceFlavourProviderService
|
||||
accountId,
|
||||
workspaces: ids.map(({ id, initialized }) => ({
|
||||
id,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
flavour: this.server.id,
|
||||
initialized,
|
||||
})),
|
||||
};
|
||||
@@ -181,7 +181,7 @@ export class CloudWorkspaceFlavourProviderService
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
this.globalState.set(
|
||||
CLOUD_WORKSPACES_CACHE_KEY + accountId,
|
||||
getCloudWorkspaceCacheKey(this.server.id) + accountId,
|
||||
sorted
|
||||
);
|
||||
if (!isEqual(this.workspaces$.value, sorted)) {
|
||||
@@ -202,7 +202,9 @@ export class CloudWorkspaceFlavourProviderService
|
||||
({ accountId }) => {
|
||||
if (accountId) {
|
||||
this.workspaces$.next(
|
||||
this.globalState.get(CLOUD_WORKSPACES_CACHE_KEY + accountId) ?? []
|
||||
this.globalState.get(
|
||||
getCloudWorkspaceCacheKey(this.server.id) + accountId
|
||||
) ?? []
|
||||
);
|
||||
} else {
|
||||
this.workspaces$.next([]);
|
||||
@@ -295,9 +297,7 @@ export class CloudWorkspaceFlavourProviderService
|
||||
|
||||
onWorkspaceInitialized(workspace: Workspace): void {
|
||||
// bind the workspace to the affine cloud server
|
||||
workspace.scope
|
||||
.get(WorkspaceServerService)
|
||||
.bindServer(this.affineCloudServer);
|
||||
workspace.scope.get(WorkspaceServerService).bindServer(this.server);
|
||||
}
|
||||
|
||||
private async getIsOwner(workspaceId: string, signal?: AbortSignal) {
|
||||
@@ -315,4 +315,61 @@ export class CloudWorkspaceFlavourProviderService
|
||||
private waitForLoaded() {
|
||||
return this.isRevalidating$.waitFor(loading => !loading);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.revalidate.unsubscribe();
|
||||
this.unsubscribeAccountChanged();
|
||||
}
|
||||
}
|
||||
|
||||
export class CloudWorkspaceFlavoursProvider
|
||||
extends Service
|
||||
implements WorkspaceFlavoursProvider
|
||||
{
|
||||
constructor(
|
||||
private readonly globalState: GlobalState,
|
||||
private readonly storageProvider: WorkspaceEngineStorageProvider,
|
||||
private readonly serversService: ServersService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
workspaceFlavours$ = LiveData.from<WorkspaceFlavourProvider[]>(
|
||||
this.serversService.servers$.pipe(
|
||||
switchMap(servers => {
|
||||
const refs = servers.map(server => {
|
||||
const exists = this.pool.get(server.id);
|
||||
if (exists) {
|
||||
return exists;
|
||||
}
|
||||
const provider = new CloudWorkspaceFlavourProvider(
|
||||
this.globalState,
|
||||
this.storageProvider,
|
||||
server
|
||||
);
|
||||
provider.revalidate();
|
||||
const ref = this.pool.put(server.id, provider);
|
||||
return ref;
|
||||
});
|
||||
|
||||
return new Observable<WorkspaceFlavourProvider[]>(subscribe => {
|
||||
subscribe.next(refs.map(ref => ref.obj));
|
||||
return () => {
|
||||
refs.forEach(ref => {
|
||||
ref.release();
|
||||
});
|
||||
};
|
||||
});
|
||||
})
|
||||
),
|
||||
[] as any
|
||||
);
|
||||
|
||||
private readonly pool = new ObjectPool<string, CloudWorkspaceFlavourProvider>(
|
||||
{
|
||||
onDelete(obj) {
|
||||
obj.dispose();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user