diff --git a/packages/backend/server/src/config/affine.ts b/packages/backend/server/src/config/affine.ts index 1c22b3e88d..0dcec63c40 100644 --- a/packages/backend/server/src/config/affine.ts +++ b/packages/backend/server/src/config/affine.ts @@ -56,7 +56,7 @@ AFFiNE.port = 3010; // /* User Signup password limitation */ // AFFiNE.auth.password = { // minLength: 8, -// maxLength: 20, +// maxLength: 32, // }; // // /* How long the login session would last by default */ diff --git a/packages/backend/server/src/core/auth/service.ts b/packages/backend/server/src/core/auth/service.ts index 6fe0c6082b..86adf281b2 100644 --- a/packages/backend/server/src/core/auth/service.ts +++ b/packages/backend/server/src/core/auth/service.ts @@ -60,7 +60,7 @@ export class AuthService implements OnApplicationBootstrap { path: '/', secure: this.config.https, }; - static readonly sessionCookieName = 'sid'; + static readonly sessionCookieName = 'affine_session'; static readonly authUserSeqHeaderName = 'x-auth-user'; constructor( @@ -299,10 +299,11 @@ export class AuthService implements OnApplicationBootstrap { } } - async setCookie(req: Request, res: Response, user: { id: string }) { + async setCookie(_req: Request, res: Response, user: { id: string }) { const session = await this.createUserSession( - user, - req.cookies[AuthService.sessionCookieName] + user + // TODO(@forehalo): enable multi user session + // req.cookies[AuthService.sessionCookieName] ); res.cookie(AuthService.sessionCookieName, session.sessionId, { diff --git a/packages/backend/server/src/core/config.ts b/packages/backend/server/src/core/config.ts index 828027df56..01830f11e3 100644 --- a/packages/backend/server/src/core/config.ts +++ b/packages/backend/server/src/core/config.ts @@ -22,6 +22,20 @@ export function ADD_ENABLED_FEATURES(feature: ServerFeature) { ENABLED_FEATURES.add(feature); } +@ObjectType() +export class PasswordLimitsType { + @Field() + minLength!: number; + @Field() + maxLength!: number; +} + +@ObjectType() +export class CredentialsRequirementType { + @Field() + password!: PasswordLimitsType; +} + @ObjectType() export class ServerConfigType { @Field({ @@ -47,6 +61,11 @@ export class ServerConfigType { @Field(() => [ServerFeature], { description: 'enabled server features' }) features!: ServerFeature[]; + + @Field(() => CredentialsRequirementType, { + description: 'credentials requirement', + }) + credentialsRequirement!: CredentialsRequirementType; } export class ServerConfigResolver { @@ -65,6 +84,9 @@ export class ServerConfigResolver { // this field should be removed after frontend feature flags implemented flavor: AFFiNE.type, features: Array.from(ENABLED_FEATURES), + credentialsRequirement: { + password: AFFiNE.auth.password, + }, }; } } diff --git a/packages/backend/server/src/core/storage/wrappers/blob.ts b/packages/backend/server/src/core/storage/wrappers/blob.ts index 84608a7aab..b60b52759e 100644 --- a/packages/backend/server/src/core/storage/wrappers/blob.ts +++ b/packages/backend/server/src/core/storage/wrappers/blob.ts @@ -1,13 +1,13 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { - BlobInputType, + type BlobInputType, Cache, EventEmitter, type EventPayload, - ListObjectsMetadata, + type ListObjectsMetadata, OnEvent, - StorageProvider, + type StorageProvider, StorageProviderFactory, } from '../../../fundamentals'; @@ -18,14 +18,14 @@ export class WorkspaceBlobStorage { constructor( private readonly event: EventEmitter, private readonly storageFactory: StorageProviderFactory, - @Inject(forwardRef(() => Cache)) private readonly cache: Cache + private readonly cache: Cache ) { this.provider = this.storageFactory.create('blob'); } async put(workspaceId: string, key: string, blob: BlobInputType) { await this.provider.put(`${workspaceId}/${key}`, blob); - await this.cache.delete(`blobs:${workspaceId}`); + await this.cache.delete(`blob-list:${workspaceId}`); } async get(workspaceId: string, key: string) { @@ -34,7 +34,7 @@ export class WorkspaceBlobStorage { async list(workspaceId: string) { const cachedList = await this.cache.list( - `blobs:${workspaceId}`, + `blob-list:${workspaceId}`, 0, -1 ); @@ -50,7 +50,7 @@ export class WorkspaceBlobStorage { item.key = item.key.slice(workspaceId.length + 1); }); - await this.cache.pushBack(`blobs:${workspaceId}`, ...blobs); + await this.cache.pushBack(`blob-list:${workspaceId}`, ...blobs); return blobs; } diff --git a/packages/backend/server/src/fundamentals/config/def.ts b/packages/backend/server/src/fundamentals/config/def.ts index 48ca191f93..c4c110be3b 100644 --- a/packages/backend/server/src/fundamentals/config/def.ts +++ b/packages/backend/server/src/fundamentals/config/def.ts @@ -214,9 +214,14 @@ export interface AFFiNEConfig { * authentication config */ auth: { + /** + * The minimum and maximum length of the password when registering new users + * + * @default [8,32] + */ password: { /** - * The minimum and maximum length of the password when registering new users + * The minimum length of the password * * @default 8 */ @@ -224,7 +229,7 @@ export interface AFFiNEConfig { /** * The maximum length of the password * - * @default 20 + * @default 32 */ maxLength: number; }; diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index e91fb65026..e76bda04f8 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -10,6 +10,10 @@ input CreateCheckoutSessionInput { successCallbackLink: String } +type CredentialsRequirementType { + password: PasswordLimitsType! +} + """ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. """ @@ -164,6 +168,11 @@ enum OAuthProviderType { Google } +type PasswordLimitsType { + maxLength: Int! + minLength: Int! +} + """User permission in workspace""" enum Permission { Admin @@ -236,6 +245,9 @@ type ServerConfigType { """server base url""" baseUrl: String! + """credentials requirement""" + credentialsRequirement: CredentialsRequirementType! + """enabled server features""" features: [ServerFeature!]! diff --git a/packages/backend/server/tests/auth/guard.spec.ts b/packages/backend/server/tests/auth/guard.spec.ts index 78dccc8905..1841b38480 100644 --- a/packages/backend/server/tests/auth/guard.spec.ts +++ b/packages/backend/server/tests/auth/guard.spec.ts @@ -73,7 +73,7 @@ test('should be able to visit public api if signed in', async t => { const res = await request(app.getHttpServer()) .get('/public') - .set('Cookie', 'sid=1') + .set('Cookie', `${AuthService.sessionCookieName}=1`) .expect(HttpStatus.OK); t.is(res.body.user.id, '1'); @@ -102,7 +102,7 @@ test('should be able to visit private api if signed in', async t => { const res = await request(app.getHttpServer()) .get('/private') - .set('Cookie', 'sid=1') + .set('Cookie', `${AuthService.sessionCookieName}=1`) .expect(HttpStatus.OK); t.is(res.body.user.id, '1'); @@ -113,7 +113,7 @@ test('should be able to parse session cookie', async t => { await request(app.getHttpServer()) .get('/public') - .set('cookie', 'sid=1') + .set('cookie', `${AuthService.sessionCookieName}=1`) .expect(200); t.deepEqual(auth.getUser.firstCall.args, ['1', 0]); diff --git a/packages/backend/server/tests/oauth/controller.spec.ts b/packages/backend/server/tests/oauth/controller.spec.ts index 542de3a0c1..bbc7984ddb 100644 --- a/packages/backend/server/tests/oauth/controller.spec.ts +++ b/packages/backend/server/tests/oauth/controller.spec.ts @@ -309,7 +309,7 @@ test('should throw if oauth account already connected', async t => { const res = await request(app.getHttpServer()) .get(`/oauth/callback?code=1&state=1`) - .set('cookie', 'sid=1') + .set('cookie', `${AuthService.sessionCookieName}=1`) .expect(HttpStatus.FOUND); const link = new URL(res.headers.location); @@ -331,7 +331,7 @@ test('should be able to connect oauth account', async t => { await request(app.getHttpServer()) .get(`/oauth/callback?code=1&state=1`) - .set('cookie', 'sid=1') + .set('cookie', `${AuthService.sessionCookieName}=1`) .expect(HttpStatus.FOUND); const account = await db.connectedAccount.findFirst({ diff --git a/packages/backend/server/tests/utils/user.ts b/packages/backend/server/tests/utils/user.ts index 8f238f5d03..8a4849d970 100644 --- a/packages/backend/server/tests/utils/user.ts +++ b/packages/backend/server/tests/utils/user.ts @@ -2,13 +2,17 @@ import type { INestApplication } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import request, { type Response } from 'supertest'; -import type { ClientTokenType, CurrentUser } from '../../src/core/auth'; +import { + AuthService, + type ClientTokenType, + type CurrentUser, +} from '../../src/core/auth'; import type { UserType } from '../../src/core/user'; import { gql } from './common'; export function sessionCookie(headers: any) { const cookie = headers['set-cookie']?.find((c: string) => - c.startsWith('sid=') + c.startsWith(`${AuthService.sessionCookieName}=`) ); if (!cookie) { diff --git a/packages/common/infra/src/page/record-list.ts b/packages/common/infra/src/page/record-list.ts index 697fb745d6..9405dfa137 100644 --- a/packages/common/infra/src/page/record-list.ts +++ b/packages/common/infra/src/page/record-list.ts @@ -11,6 +11,8 @@ export class PageRecordList { private readonly localState: WorkspaceLocalState ) {} + private readonly recordsPool = new Map(); + public readonly records$ = LiveData.from( new Observable(subscriber => { const emit = () => { @@ -29,7 +31,15 @@ export class PageRecordList { }).pipe( distinctUntilChanged((p, c) => isEqual(p, c)), map(ids => - ids.map(id => new PageRecord(id, this.workspace, this.localState)) + ids.map(id => { + const exists = this.recordsPool.get(id); + if (exists) { + return exists; + } + const record = new PageRecord(id, this.workspace, this.localState); + this.recordsPool.set(id, record); + return record; + }) ) ), [] diff --git a/packages/common/infra/src/page/record.ts b/packages/common/infra/src/page/record.ts index b117ef3245..faf7f3bda6 100644 --- a/packages/common/infra/src/page/record.ts +++ b/packages/common/infra/src/page/record.ts @@ -1,5 +1,6 @@ import type { DocMeta } from '@blocksuite/store'; -import { Observable } from 'rxjs'; +import { isEqual } from 'lodash-es'; +import { distinctUntilChanged, Observable } from 'rxjs'; import { LiveData } from '../livedata'; import type { Workspace, WorkspaceLocalState } from '../workspace'; @@ -13,8 +14,8 @@ export class PageRecord { private readonly localState: WorkspaceLocalState ) {} - meta$ = LiveData.from( - new Observable(subscriber => { + meta$ = LiveData.from>( + new Observable(subscriber => { const emit = () => { const meta = this.workspace.docCollection.meta.docMetas.find( page => page.id === this.id @@ -32,7 +33,7 @@ export class PageRecord { return () => { dispose(); }; - }), + }).pipe(distinctUntilChanged((p, c) => isEqual(p, c))), { id: this.id, title: '', @@ -59,5 +60,5 @@ export class PageRecord { return this.mode$.value; } - title$ = this.meta$.map(meta => meta.title); + title$ = this.meta$.map(meta => meta.title ?? ''); } diff --git a/packages/common/infra/src/workspace/engine/index.ts b/packages/common/infra/src/workspace/engine/index.ts index 50b8b46d34..645e16046d 100644 --- a/packages/common/infra/src/workspace/engine/index.ts +++ b/packages/common/infra/src/workspace/engine/index.ts @@ -42,6 +42,7 @@ export class WorkspaceEngine { blob: status, }; }); + this.doc.setPriority(yDoc.guid, 100); this.doc.addDoc(yDoc); } diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index ad49eb91f9..378865bd9e 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -70,7 +70,8 @@ "react-virtuoso": "^4.7.0", "rxjs": "^7.8.1", "swr": "^2.2.5", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "zod": "^3.22.4" }, "devDependencies": { "@blocksuite/block-std": "0.14.0-canary-202403250855-4171ecd", diff --git a/packages/frontend/component/src/components/auth-components/change-password-page.tsx b/packages/frontend/component/src/components/auth-components/change-password-page.tsx index cb11b6898e..f4b1f66749 100644 --- a/packages/frontend/component/src/components/auth-components/change-password-page.tsx +++ b/packages/frontend/component/src/components/auth-components/change-password-page.tsx @@ -1,3 +1,4 @@ +import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useSetAtom } from 'jotai'; import type { FC } from 'react'; @@ -11,9 +12,15 @@ import type { User } from './type'; export const ChangePasswordPage: FC<{ user: User; + passwordLimits: PasswordLimitsFragment; onSetPassword: (password: string) => Promise; onOpenAffine: () => void; -}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { +}> = ({ + user: { email }, + passwordLimits, + onSetPassword: propsOnSetPassword, + onOpenAffine, +}) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); const pushNotification = useSetAtom(pushNotificationAtom); @@ -46,7 +53,10 @@ export const ChangePasswordPage: FC<{ t['com.affine.auth.sent.reset.password.success.message']() ) : ( <> - {t['com.affine.auth.page.sent.email.subtitle']()} + {t['com.affine.auth.page.sent.email.subtitle']({ + min: String(passwordLimits.minLength), + max: String(passwordLimits.maxLength), + })} {email} ) @@ -57,7 +67,10 @@ export const ChangePasswordPage: FC<{ {t['com.affine.auth.open.affine']()} ) : ( - + )} ); diff --git a/packages/frontend/component/src/components/auth-components/password-input/index.tsx b/packages/frontend/component/src/components/auth-components/password-input/index.tsx index 223d39349a..ff8567fb24 100644 --- a/packages/frontend/component/src/components/auth-components/password-input/index.tsx +++ b/packages/frontend/component/src/components/auth-components/password-input/index.tsx @@ -1,104 +1,199 @@ +import { type PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { passwordStrength } from 'check-password-strength'; -import type { FC } from 'react'; -import { useCallback, useEffect, useState } from 'react'; +import { type Options, passwordStrength } from 'check-password-strength'; +import { type FC, useEffect, useMemo } from 'react'; +import { useCallback, useState } from 'react'; +import { z, type ZodCustomIssue, ZodIssueCode } from 'zod'; import type { InputProps } from '../../../ui/input'; import { Input } from '../../../ui/input'; import * as styles from '../share.css'; import { ErrorIcon } from './error'; +import { statusWrapper } from './style.css'; import { SuccessIcon } from './success'; import { Tag } from './tag'; -export type Status = 'weak' | 'medium' | 'strong' | 'maximum'; +export type Status = 'weak' | 'medium' | 'strong' | 'minimum' | 'maximum'; + +const PASSWORD_STRENGTH_OPTIONS: Options = [ + { + id: 0, + value: 'weak', + minDiversity: 0, + minLength: 0, + }, + { + id: 1, + value: 'medium', + minDiversity: 4, + minLength: 8, + }, + { + id: 2, + value: 'strong', + minDiversity: 4, + minLength: 10, + }, +]; export const PasswordInput: FC< InputProps & { + passwordLimits: PasswordLimitsFragment; onPass: (password: string) => void; onPrevent: () => void; } -> = ({ onPass, onPrevent, ...inputProps }) => { +> = ({ passwordLimits, onPass, onPrevent, ...inputProps }) => { const t = useAFFiNEI18N(); const [status, setStatus] = useState(null); const [confirmStatus, setConfirmStatus] = useState< 'success' | 'error' | null >(null); + const [canSubmit, setCanSubmit] = useState(false); const [password, setPassWord] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const validationSchema = useMemo(() => { + const { minLength, maxLength } = passwordLimits; + return z.string().superRefine((val, ctx) => { + if (val.length < minLength) { + ctx.addIssue({ + code: ZodIssueCode.custom, + params: { + status: 'minimum', + }, + }); + } else if (val.length > maxLength) { + ctx.addIssue({ + code: ZodIssueCode.custom, + params: { + status: 'maximum', + }, + }); + } + // https://github.com/deanilvincent/check-password-strength?tab=readme-ov-file#default-options + const { value: status } = passwordStrength( + val, + PASSWORD_STRENGTH_OPTIONS + ); - const onPasswordChange = useCallback((value: string) => { - setPassWord(value); - if (!value) { - return setStatus(null); - } - if (value.length > 20) { - return setStatus('maximum'); - } - switch (passwordStrength(value).id) { - case 0: - case 1: - setStatus('weak'); - break; - case 2: - setStatus('medium'); - break; - case 3: - setStatus('strong'); - break; - } - }, []); + ctx.addIssue({ + code: ZodIssueCode.custom, + message: 'password strength', + path: ['strength'], + params: { + status, + }, + }); + }); + }, [passwordLimits]); - const onConfirmPasswordChange = useCallback((value: string) => { - setConfirmPassword(value); - }, []); + const validatePasswords = useCallback( + (password: string, confirmPassword: string) => { + const result = validationSchema.safeParse(password); + let canSubmit = false; + if (!result.success) { + const issues = result.error.issues as ZodCustomIssue[]; + const firstIssue = issues[0]; + setStatus(firstIssue.params?.status || null); + // ignore strength error + if (firstIssue.path.includes('strength')) { + canSubmit = true; + } + } + if (confirmPassword) { + const isEqual = password === confirmPassword; + if (isEqual) { + setConfirmStatus('success'); + } else { + setConfirmStatus('error'); + } + canSubmit &&= isEqual; + } else { + canSubmit &&= false; + setConfirmStatus(null); + } + setCanSubmit(canSubmit); + }, + [validationSchema] + ); + + const onPasswordChange = useCallback( + (value: string) => { + const password = value.trim(); + setPassWord(password); + validatePasswords(password, confirmPassword); + }, + [validatePasswords, confirmPassword] + ); + + const onConfirmPasswordChange = useCallback( + (value: string) => { + const confirmPassword = value.trim(); + setConfirmPassword(confirmPassword); + validatePasswords(password, confirmPassword); + }, + [validatePasswords, password] + ); useEffect(() => { - if (!password || !confirmPassword) { - return; - } - if (password === confirmPassword) { - setConfirmStatus('success'); - } else { - setConfirmStatus('error'); - } - }, [confirmPassword, password]); - - useEffect(() => { - if (confirmStatus === 'success' && password.length > 7) { + if (canSubmit) { onPass(password); } else { onPrevent(); } - }, [confirmStatus, onPass, onPrevent, password]); + }, [canSubmit, password, onPass, onPrevent]); return ( <> : null} + endFix={ +
+ {status ? ( + + ) : null} +
+ } {...inputProps} /> - ) : ( - - ) - ) : null +
+ {confirmStatus ? ( + confirmStatus === 'success' ? ( + + ) : ( + + ) + ) : null} +
} {...inputProps} /> diff --git a/packages/frontend/component/src/components/auth-components/password-input/style.css.ts b/packages/frontend/component/src/components/auth-components/password-input/style.css.ts index 2d9e66e2e4..738f7ba8ba 100644 --- a/packages/frontend/component/src/components/auth-components/password-input/style.css.ts +++ b/packages/frontend/component/src/components/auth-components/password-input/style.css.ts @@ -1,7 +1,11 @@ import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; +export const statusWrapper = style({ + marginLeft: 8, + marginRight: 10, +}); export const tag = style({ - padding: '0 15px', + padding: '2px 15px', height: 20, lineHeight: '20px', borderRadius: 10, @@ -19,7 +23,7 @@ export const tag = style({ backgroundColor: cssVar('tagGreen'), color: cssVar('successColor'), }, - '&.maximum': { + '&.minimum, &.maximum': { backgroundColor: cssVar('tagRed'), color: cssVar('errorColor'), }, diff --git a/packages/frontend/component/src/components/auth-components/password-input/tag.tsx b/packages/frontend/component/src/components/auth-components/password-input/tag.tsx index ccaf06c394..9a0702ee56 100644 --- a/packages/frontend/component/src/components/auth-components/password-input/tag.tsx +++ b/packages/frontend/component/src/components/auth-components/password-input/tag.tsx @@ -4,15 +4,23 @@ import { useMemo } from 'react'; import type { Status } from './index'; import { tag } from './style.css'; -export const Tag: FC<{ status: Status }> = ({ status }) => { + +type TagProps = { + status: Status; + minimum: string; + maximum: string; +}; + +export const Tag: FC = ({ status, minimum, maximum }) => { const textMap = useMemo<{ [K in Status]: string }>(() => { return { weak: 'Weak', medium: 'Medium', strong: 'Strong', - maximum: 'Maximum', + minimum, + maximum, }; - }, []); + }, [minimum, maximum]); return (
= ({ status }) => { weak: status === 'weak', medium: status === 'medium', strong: status === 'strong', + minimum: status === 'minimum', maximum: status === 'maximum', })} > diff --git a/packages/frontend/component/src/components/auth-components/set-password-page.tsx b/packages/frontend/component/src/components/auth-components/set-password-page.tsx index 09639d596a..6859707ba9 100644 --- a/packages/frontend/component/src/components/auth-components/set-password-page.tsx +++ b/packages/frontend/component/src/components/auth-components/set-password-page.tsx @@ -1,3 +1,4 @@ +import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useSetAtom } from 'jotai'; import type { FC } from 'react'; @@ -11,9 +12,15 @@ import type { User } from './type'; export const SetPasswordPage: FC<{ user: User; + passwordLimits: PasswordLimitsFragment; onSetPassword: (password: string) => Promise; onOpenAffine: () => void; -}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { +}> = ({ + user: { email }, + passwordLimits, + onSetPassword: propsOnSetPassword, + onOpenAffine, +}) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); const pushNotification = useSetAtom(pushNotificationAtom); @@ -46,7 +53,10 @@ export const SetPasswordPage: FC<{ t['com.affine.auth.sent.set.password.success.message']() ) : ( <> - {t['com.affine.auth.page.sent.email.subtitle']()} + {t['com.affine.auth.page.sent.email.subtitle']({ + min: String(passwordLimits.minLength), + max: String(passwordLimits.maxLength), + })} {email} ) @@ -57,7 +67,10 @@ export const SetPasswordPage: FC<{ {t['com.affine.auth.open.affine']()} ) : ( - + )} ); diff --git a/packages/frontend/component/src/components/auth-components/set-password.tsx b/packages/frontend/component/src/components/auth-components/set-password.tsx index c77195518e..9989d51878 100644 --- a/packages/frontend/component/src/components/auth-components/set-password.tsx +++ b/packages/frontend/component/src/components/auth-components/set-password.tsx @@ -1,3 +1,4 @@ +import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { FC } from 'react'; import { useCallback, useRef, useState } from 'react'; @@ -7,10 +8,11 @@ import { Wrapper } from '../../ui/layout'; import { PasswordInput } from './password-input'; export const SetPassword: FC<{ + passwordLimits: PasswordLimitsFragment; showLater?: boolean; onLater?: () => void; onSetPassword: (password: string) => void; -}> = ({ onLater, onSetPassword, showLater = false }) => { +}> = ({ passwordLimits, onLater, onSetPassword, showLater = false }) => { const t = useAFFiNEI18N(); const [passwordPass, setPasswordPass] = useState(false); @@ -20,6 +22,7 @@ export const SetPassword: FC<{ <> { setPasswordPass(true); passwordRef.current = password; diff --git a/packages/frontend/component/src/components/auth-components/sign-up-page.tsx b/packages/frontend/component/src/components/auth-components/sign-up-page.tsx index 684b6cdd9e..11a4804fc1 100644 --- a/packages/frontend/component/src/components/auth-components/sign-up-page.tsx +++ b/packages/frontend/component/src/components/auth-components/sign-up-page.tsx @@ -1,3 +1,4 @@ +import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useSetAtom } from 'jotai'; import type { FC } from 'react'; @@ -10,11 +11,13 @@ import { SetPassword } from './set-password'; import type { User } from './type'; export const SignUpPage: FC<{ + passwordLimits: PasswordLimitsFragment; user: User; onSetPassword: (password: string) => Promise; openButtonText?: string; onOpenAffine: () => void; }> = ({ + passwordLimits, user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine, @@ -55,7 +58,10 @@ export const SignUpPage: FC<{ t['com.affine.auth.sign.up.success.subtitle']() ) : ( <> - {t['com.affine.auth.page.sent.email.subtitle']()} + {t['com.affine.auth.page.sent.email.subtitle']({ + min: String(passwordLimits.minLength), + max: String(passwordLimits.maxLength), + })} {email} ) @@ -67,6 +73,7 @@ export const SignUpPage: FC<{ ) : ( { return t['com.affine.settings.email.action.verify'](); } }; -const useContent = (emailType: AuthPanelProps['emailType'], email: string) => { +const useContent = ( + emailType: AuthPanelProps['emailType'], + email: string, + passwordLimits: PasswordLimitsFragment +) => { const t = useAFFiNEI18N(); switch (emailType) { case 'setPassword': - return t['com.affine.auth.set.password.message'](); + return t['com.affine.auth.set.password.message']({ + min: String(passwordLimits.minLength), + max: String(passwordLimits.maxLength), + }); case 'changePassword': return t['com.affine.auth.reset.password.message'](); case 'changeEmail': @@ -154,12 +163,13 @@ export const SendEmail = ({ emailType, }: AuthPanelProps) => { const t = useAFFiNEI18N(); + const { password: passwordLimits } = useCredentialsRequirement(); const [hasSentEmail, setHasSentEmail] = useState(false); const pushNotification = useSetAtom(pushNotificationAtom); const title = useEmailTitle(emailType); const hint = useNotificationHint(emailType); - const content = useContent(emailType, email); + const content = useContent(emailType, email, passwordLimits); const buttonContent = useButtonContent(emailType); const { loading, sendEmail } = useSendEmail(emailType); diff --git a/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx b/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx index 56271fe189..65a2987fdc 100644 --- a/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx +++ b/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx @@ -2,6 +2,7 @@ import { SignUpPage } from '@affine/component/auth-components'; import { Button } from '@affine/component/ui/button'; import { Loading } from '@affine/component/ui/loading'; import { AffineShapeIcon } from '@affine/core/components/page-list'; +import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import type { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql'; import { @@ -106,6 +107,7 @@ const SubscriptionRedirectWithData = () => { const user = useCurrentUser(); const searchData = useSubscriptionSearch(); const openPaymentUrl = usePaymentRedirect(); + const { password: passwordLimits } = useCredentialsRequirement(); const { trigger: changePassword } = useMutation({ mutation: changePasswordMutation, @@ -128,6 +130,7 @@ const SubscriptionRedirectWithData = () => { return ( { desc={t['com.affine.telemetry.enable.desc']()} > diff --git a/packages/frontend/core/src/components/pure/cmdk/main.tsx b/packages/frontend/core/src/components/pure/cmdk/main.tsx index 9b0680ff97..36c00d6225 100644 --- a/packages/frontend/core/src/components/pure/cmdk/main.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/main.tsx @@ -155,7 +155,7 @@ export const CMDKContainer = ({ open: boolean; className?: string; query: string; - pageMeta?: DocMeta; + pageMeta?: Partial; groups: ReturnType; onQueryChange: (query: string) => void; }>) => { @@ -234,7 +234,7 @@ const CMDKQuickSearchModalInner = ({ pageMeta, open, ...props -}: CMDKModalProps & { pageMeta?: DocMeta }) => { +}: CMDKModalProps & { pageMeta?: Partial }) => { const [query, setQuery] = useAtom(cmdkQueryAtom); useLayoutEffect(() => { if (open) { @@ -260,7 +260,7 @@ export const CMDKQuickSearchModal = ({ pageMeta, open, ...props -}: CMDKModalProps & { pageMeta?: DocMeta }) => { +}: CMDKModalProps & { pageMeta?: Partial }) => { return ( }> diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx index d0ee2b3ebb..3f6acc502e 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx @@ -192,19 +192,32 @@ const useSyncEngineSyncProgress = () => { ), active: currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD && - (syncing || retrying || isOverCapacity), + ((syncing && progress !== undefined) || isOverCapacity || !isOnline), }; }; const WorkspaceInfo = ({ name }: { name: string }) => { - const { message, icon, active } = useSyncEngineSyncProgress(); + const { message, active } = useSyncEngineSyncProgress(); const currentWorkspace = useService(Workspace); const isCloud = currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD; + const { progress } = useDocEngineStatus(); // to make sure that animation will play first time const [delayActive, setDelayActive] = useState(false); useEffect(() => { - setDelayActive(active); + const delayOpen = 300; + const delayClose = 200; + let timer: ReturnType; + if (active) { + timer = setTimeout(() => { + setDelayActive(active); + }, delayOpen); + } else { + timer = setTimeout(() => { + setDelayActive(active); + }, delayClose); + } + return () => clearTimeout(timer); }, [active]); return ( @@ -221,9 +234,11 @@ const WorkspaceInfo = ({ name }: { name: string }) => { {/* when syncing/offline/... */}
-
- {icon} -
+ +
+ +
+
diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.css.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.css.ts index 528cabf5e7..42b56e7389 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.css.ts +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.css.ts @@ -4,7 +4,7 @@ import { globalStyle, style } from '@vanilla-extract/css'; const wsSlideAnim = { ease: 'cubic-bezier(.45,.21,0,1)', duration: '0.5s', - delay: '0.23s', + delay: '0s', }; export const container = style({ diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.css.ts b/packages/frontend/core/src/components/root-app-sidebar/index.css.ts index 1b4d57eee5..684f679db5 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.css.ts +++ b/packages/frontend/core/src/components/root-app-sidebar/index.css.ts @@ -1,4 +1,4 @@ -import { style } from '@vanilla-extract/css'; +import { globalStyle, style } from '@vanilla-extract/css'; export const workspaceAndUserWrapper = style({ display: 'flex', @@ -14,6 +14,12 @@ export const workspaceWrapper = style({ export const userInfoWrapper = style({ flexShrink: 0, - width: 28, - height: 28, + width: 'auto', + height: 'auto', + padding: '4px 0', +}); + +// TODO: +globalStyle(`button.${userInfoWrapper} > span`, { + lineHeight: 0, }); diff --git a/packages/frontend/core/public/imgs/unknown-user.svg b/packages/frontend/core/src/components/root-app-sidebar/unknow-user.tsx similarity index 66% rename from packages/frontend/core/public/imgs/unknown-user.svg rename to packages/frontend/core/src/components/root-app-sidebar/unknow-user.tsx index 25b33b446b..1a7ac04758 100644 --- a/packages/frontend/core/public/imgs/unknown-user.svg +++ b/packages/frontend/core/src/components/root-app-sidebar/unknow-user.tsx @@ -1,4 +1,8 @@ - +import { memo } from 'react'; + +export const UnknownUserIcon = memo( + ({ width = 20, height = 20 }: { width?: number; height?: number }) => { + const svgRaw = ` - - \ No newline at end of file +`; + + return
; + } +); +UnknownUserIcon.displayName = 'UnknownUserIcon'; diff --git a/packages/frontend/core/src/components/root-app-sidebar/user-info.tsx b/packages/frontend/core/src/components/root-app-sidebar/user-info.tsx index def23c96b0..f1b8b127a6 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/user-info.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/user-info.tsx @@ -18,11 +18,11 @@ import { } from '@affine/core/hooks/affine/use-current-user'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { AccountIcon, SignOutIcon } from '@blocksuite/icons'; -import { cssVar } from '@toeverything/theme'; import { useSetAtom } from 'jotai'; import { useCallback } from 'react'; import * as styles from './index.css'; +import { UnknownUserIcon } from './unknow-user'; export const UserInfo = () => { const { status } = useSession(); @@ -39,7 +39,7 @@ const AuthorizedUserInfo = () => { type="plain" className={styles.userInfoWrapper} > - + ); @@ -61,11 +61,7 @@ const UnauthorizedUserInfo = () => { type="plain" className={styles.userInfoWrapper} > - + ); }; diff --git a/packages/frontend/core/src/hooks/affine/use-server-config.ts b/packages/frontend/core/src/hooks/affine/use-server-config.ts index f6cf86146e..6203b2756e 100644 --- a/packages/frontend/core/src/hooks/affine/use-server-config.ts +++ b/packages/frontend/core/src/hooks/affine/use-server-config.ts @@ -1,4 +1,4 @@ -import type { ServerFeature } from '@affine/graphql'; +import type { ServerConfigQuery, ServerFeature } from '@affine/graphql'; import { oauthProvidersQuery, serverConfigQuery } from '@affine/graphql'; import type { BareFetcher, Middleware } from 'swr'; @@ -73,3 +73,13 @@ export const useServerBaseUrl = () => { return config.baseUrl; }; + +export const useCredentialsRequirement = () => { + const config = useServerConfig(); + + if (!config) { + return {} as ServerConfigQuery['serverConfig']['credentialsRequirement']; + } + + return config.credentialsRequirement; +}; diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.tsx index d1a15194ec..71252aa9e6 100644 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.tsx +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.tsx @@ -316,7 +316,7 @@ const ConflictList = ({ setTrashModal({ open: true, pageIds: [pageRecord.id], - pageTitles: [pageRecord.meta$.value.title], + pageTitles: [pageRecord.title$.value], }); }, [setTrashModal] diff --git a/packages/frontend/core/src/modules/tag/entities/tag.ts b/packages/frontend/core/src/modules/tag/entities/tag.ts index bc1f8f7b3c..4550021946 100644 --- a/packages/frontend/core/src/modules/tag/entities/tag.ts +++ b/packages/frontend/core/src/modules/tag/entities/tag.ts @@ -50,7 +50,7 @@ export class Tag { return; } pageRecord?.setMeta({ - tags: [...pageRecord.meta$.value.tags, this.id], + tags: [...(pageRecord.meta$.value.tags ?? []), this.id], }); } @@ -60,14 +60,14 @@ export class Tag { return; } pageRecord?.setMeta({ - tags: pageRecord.meta$.value.tags.filter(tagId => tagId !== this.id), + tags: pageRecord.meta$.value.tags?.filter(tagId => tagId !== this.id), }); } readonly pageIds$ = LiveData.computed(get => { const pages = get(this.pageRecordList.records$); return pages - .filter(page => get(page.meta$).tags.includes(this.id)) + .filter(page => get(page.meta$).tags?.includes(this.id)) .map(page => page.id); }); } diff --git a/packages/frontend/core/src/modules/tag/service/tag.ts b/packages/frontend/core/src/modules/tag/service/tag.ts index f2231bfacc..e1d6a25b89 100644 --- a/packages/frontend/core/src/modules/tag/service/tag.ts +++ b/packages/frontend/core/src/modules/tag/service/tag.ts @@ -41,7 +41,7 @@ export class TagService { if (!pageRecord) return []; const tagIds = get(pageRecord.meta$).tags; - return get(this.tags$).filter(tag => tagIds.includes(tag.id)); + return get(this.tags$).filter(tag => (tagIds ?? []).includes(tag.id)); }); } diff --git a/packages/frontend/core/src/pages/auth.tsx b/packages/frontend/core/src/pages/auth.tsx index 732ffe2bdf..d70850dd28 100644 --- a/packages/frontend/core/src/pages/auth.tsx +++ b/packages/frontend/core/src/pages/auth.tsx @@ -8,6 +8,7 @@ import { SignUpPage, } from '@affine/component/auth-components'; import { pushNotificationAtom } from '@affine/component/notification-center'; +import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config'; import { changeEmailMutation, changePasswordMutation, @@ -45,6 +46,7 @@ const authTypeSchema = z.enum([ export const AuthPage = (): ReactElement | null => { const user = useCurrentUser(); const t = useAFFiNEI18N(); + const { password: passwordLimits } = useCredentialsRequirement(); const { authType } = useParams(); const [searchParams] = useSearchParams(); @@ -112,6 +114,7 @@ export const AuthPage = (): ReactElement | null => { return ( @@ -124,6 +127,7 @@ export const AuthPage = (): ReactElement | null => { return ( @@ -133,6 +137,7 @@ export const AuthPage = (): ReactElement | null => { return ( diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index 0c225c73d9..93bd3b6a31 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -27,13 +27,14 @@ import { } from '@toeverything/infra'; import clsx from 'clsx'; import { useSetAtom } from 'jotai'; -import type { ReactElement } from 'react'; +import type { KeyboardEventHandler, ReactElement } from 'react'; import { memo, useCallback, useEffect, useLayoutEffect, useMemo, + useRef, useState, } from 'react'; import { useParams } from 'react-router-dom'; @@ -163,7 +164,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { const updatedDate = linkedPage.meta$.value.updatedDate; const createDate = linkedPage.meta$.value.createDate; - return updatedDate ? new Date(updatedDate) : new Date(createDate); + return new Date(updatedDate || createDate || Date.now()); }; page.setMode(mode); @@ -202,6 +203,17 @@ const DetailPageImpl = memo(function DetailPageImpl() { ); const isWindowsDesktop = environment.isDesktop && environment.isWindows; + const scrollableRef = useRef(null); + const keydownHandler: KeyboardEventHandler = useCallback(e => { + // prevent pgup/pgdown from scrolling the page + if (e.key === 'PageUp' || e.key === 'PageDown') { + e.preventDefault(); + scrollableRef.current?.scrollBy({ + top: e.key === 'PageUp' ? -window.innerHeight : window.innerHeight, + behavior: 'smooth', + }); + } + }, []); return ( <> @@ -218,6 +230,8 @@ const DetailPageImpl = memo(function DetailPageImpl() { { const t = useAFFiNEI18N(); return ( @@ -35,13 +33,6 @@ const TrashHeader = () => { {t['com.affine.workspaceSubPath.trash']()}
} - right={ - isWindowsDesktop ? ( -
- -
- ) : null - } /> ); }; diff --git a/packages/frontend/core/src/telemetry.tsx b/packages/frontend/core/src/telemetry.tsx new file mode 100644 index 0000000000..d3830c6413 --- /dev/null +++ b/packages/frontend/core/src/telemetry.tsx @@ -0,0 +1,20 @@ +import { appSettingAtom } from '@toeverything/infra'; +import { useAtomValue } from 'jotai/react'; +import mixpanel from 'mixpanel-browser'; +import { useLayoutEffect } from 'react'; + +export function Telemetry() { + const settings = useAtomValue(appSettingAtom); + useLayoutEffect(() => { + if (process.env.MIXPANEL_TOKEN) { + mixpanel.init(process.env.MIXPANEL_TOKEN || '', { + track_pageview: true, + persistence: 'localStorage', + }); + } + if (settings.enableTelemetry !== false) { + mixpanel.opt_out_tracking(); + } + }, [settings.enableTelemetry]); + return null; +} diff --git a/packages/frontend/electron/package.json b/packages/frontend/electron/package.json index ca19e81f42..ed1930fac7 100644 --- a/packages/frontend/electron/package.json +++ b/packages/frontend/electron/package.json @@ -44,7 +44,6 @@ "@emotion/react": "^11.11.4", "@pengx17/electron-forge-maker-appimage": "^1.1.1", "@toeverything/infra": "workspace:*", - "@types/mixpanel-browser": "^2.49.0", "@types/uuid": "^9.0.8", "@vitejs/plugin-react-swc": "^3.6.0", "builder-util-runtime": "^9.2.4", @@ -60,7 +59,6 @@ "jotai": "^2.6.5", "jotai-devtools": "^0.8.0", "lodash-es": "^4.17.21", - "mixpanel-browser": "^2.49.0", "nanoid": "^5.0.6", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/frontend/electron/renderer/app.tsx b/packages/frontend/electron/renderer/app.tsx index 67bab60ea2..a4076efbc4 100644 --- a/packages/frontend/electron/renderer/app.tsx +++ b/packages/frontend/electron/renderer/app.tsx @@ -12,12 +12,12 @@ import { performanceLogger, performanceRenderLogger, } from '@affine/core/shared'; +import { Telemetry } from '@affine/core/telemetry'; import createEmotionCache from '@affine/core/utils/create-emotion-cache'; import { configureWebServices } from '@affine/core/web'; import { createI18n, setUpLanguage } from '@affine/i18n'; import { CacheProvider } from '@emotion/react'; import { getCurrentStore, ServiceCollection } from '@toeverything/infra'; -import mixpanel from 'mixpanel-browser'; import type { PropsWithChildren, ReactElement } from 'react'; import { lazy, Suspense } from 'react'; import { RouterProvider } from 'react-router-dom'; @@ -62,13 +62,6 @@ const serviceProvider = services.provider(); export function App() { performanceRenderLogger.info('App'); - if (process.env.MIXPANEL_TOKEN) { - mixpanel.init(process.env.MIXPANEL_TOKEN || '', { - track_pageview: true, - persistence: 'localStorage', - }); - } - if (!languageLoadingPromise) { languageLoadingPromise = loadLanguage().catch(console.error); } @@ -79,6 +72,7 @@ export function App() { + diff --git a/packages/frontend/electron/src/main/deep-link.ts b/packages/frontend/electron/src/main/deep-link.ts index 862f145bd8..7275ae5399 100644 --- a/packages/frontend/electron/src/main/deep-link.ts +++ b/packages/frontend/electron/src/main/deep-link.ts @@ -88,7 +88,7 @@ async function handleOauthJwt(url: string) { httpOnly: true, value: token, secure: true, - name: 'sid', + name: 'affine_session', expirationDate: Math.floor(Date.now() / 1000 + 3600 * 24 * 7), }); diff --git a/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql b/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql new file mode 100644 index 0000000000..e76587f8bc --- /dev/null +++ b/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql @@ -0,0 +1,5 @@ +fragment CredentialsRequirement on CredentialsRequirementType { + password { + ...PasswordLimits + } +} diff --git a/packages/frontend/graphql/src/graphql/fragments/password-limits.gql b/packages/frontend/graphql/src/graphql/fragments/password-limits.gql new file mode 100644 index 0000000000..27cbaa5810 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/fragments/password-limits.gql @@ -0,0 +1,4 @@ +fragment PasswordLimits on PasswordLimitsType { + minLength + maxLength +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index 6c1acd4517..188af142f8 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -7,6 +7,17 @@ export interface GraphQLQuery { containsFile?: boolean; } +export const passwordLimitsFragment = ` +fragment PasswordLimits on PasswordLimitsType { + minLength + maxLength +}` +export const credentialsRequirementFragment = ` +fragment CredentialsRequirement on CredentialsRequirementType { + password { + ...PasswordLimits + } +}` export const checkBlobSizesQuery = { id: 'checkBlobSizesQuery' as const, operationName: 'checkBlobSizes', @@ -708,8 +719,12 @@ query serverConfig { name features type + credentialsRequirement { + ...CredentialsRequirement + } } -}`, +}${passwordLimitsFragment} +${credentialsRequirementFragment}`, }; export const setWorkspacePublicByIdMutation = { diff --git a/packages/frontend/graphql/src/graphql/server-config.gql b/packages/frontend/graphql/src/graphql/server-config.gql index b92710164f..ef17ef188a 100644 --- a/packages/frontend/graphql/src/graphql/server-config.gql +++ b/packages/frontend/graphql/src/graphql/server-config.gql @@ -1,3 +1,6 @@ +#import './fragments/password-limits.gql' +#import './fragments/credentials-requirement.gql' + query serverConfig { serverConfig { version @@ -5,5 +8,8 @@ query serverConfig { name features type + credentialsRequirement { + ...CredentialsRequirement + } } } diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index c3a40dd9be..258ab904f4 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -292,6 +292,21 @@ export type RemoveEarlyAccessMutation = { removeEarlyAccess: number; }; +export type CredentialsRequirementFragment = { + __typename?: 'CredentialsRequirementType'; + password: { + __typename?: 'PasswordLimitsType'; + minLength: number; + maxLength: number; + }; +}; + +export type PasswordLimitsFragment = { + __typename?: 'PasswordLimitsType'; + minLength: number; + maxLength: number; +}; + export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>; export type GetCurrentUserQuery = { @@ -701,6 +716,14 @@ export type ServerConfigQuery = { name: string; features: Array; type: ServerDeploymentType; + credentialsRequirement: { + __typename?: 'CredentialsRequirementType'; + password: { + __typename?: 'PasswordLimitsType'; + minLength: number; + maxLength: number; + }; + }; }; }; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index d7c274ac35..e1f7dfaedc 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -423,7 +423,7 @@ "com.affine.auth.open.affine.download-app": "Download App", "com.affine.auth.open.affine.prompt": "Opening <1>AFFiNE app now", "com.affine.auth.open.affine.try-again": "Try again", - "com.affine.auth.page.sent.email.subtitle": "Please set a password of 8-20 characters with both letters and numbers to continue signing up with ", + "com.affine.auth.page.sent.email.subtitle": "Please set a password of {{min}}-{{max}} characters with both letters and numbers to continue signing up with ", "com.affine.auth.page.sent.email.title": "Welcome to AFFiNE Cloud, you are almost there!", "com.affine.auth.password": "Password", "com.affine.auth.password.error": "Invalid password", @@ -444,10 +444,12 @@ "com.affine.auth.sent.set.password.success.message": "Your password has saved! You can sign in AFFiNE Cloud with email and password!", "com.affine.auth.set.email.save": "Save Email", "com.affine.auth.set.password": "Set password", - "com.affine.auth.set.password.message": "Please set a password of 8-20 characters with both letters and numbers to continue signing up with ", + "com.affine.auth.set.password.message": "Please set a password of {{min}}-{{max}} characters with both letters and numbers to continue signing up with ", + "com.affine.auth.set.password.message.minlength": "Minimum {{min}} characters", + "com.affine.auth.set.password.message.maxlength": "Maximum {{max}} characters", "com.affine.auth.set.password.page.success": "Password set successful", "com.affine.auth.set.password.page.title": "Set your AFFiNE Cloud password", - "com.affine.auth.set.password.placeholder": "Set a password at least 8 letters long", + "com.affine.auth.set.password.placeholder": "Set a password at least {{min}} letters long", "com.affine.auth.set.password.placeholder.confirm": "Confirm password", "com.affine.auth.set.password.save": "Save Password", "com.affine.auth.sign-out.confirm-modal.cancel": "Cancel", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index b2e1f21a68..4cc1dffb73 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -409,7 +409,7 @@ "com.affine.auth.open.affine.download-app": "下载应用", "com.affine.auth.open.affine.prompt": "正在打开 <1>AFFiNE 应用\n", "com.affine.auth.open.affine.try-again": "重试", - "com.affine.auth.page.sent.email.subtitle": "请输入一个长度在8-20个字符之间,同时包含字母和数字的密码以继续注册", + "com.affine.auth.page.sent.email.subtitle": "请输入一个长度在 {{min}}-{{max}} 个字符之间,同时包含字母和数字的密码以继续注册", "com.affine.auth.page.sent.email.title": "欢迎来到 AFFiNE Cloud,即将完成!", "com.affine.auth.password": "密码", "com.affine.auth.password.error": "无效密码", @@ -429,10 +429,12 @@ "com.affine.auth.sent.set.password.success.message": "您的密码已保存!您可以使用邮箱和密码登录 AFFiNE Cloud!", "com.affine.auth.set.email.save": "保存电子邮件", "com.affine.auth.set.password": "设置密码", - "com.affine.auth.set.password.message": "请输入一个长度在8-20个字符之间,同时包含字母和数字的密码以继续注册", + "com.affine.auth.set.password.message": "请输入一个长度在 {{min}}-{{max}} 个字符之间,同时包含字母和数字的密码以继续注册", + "com.affine.auth.set.password.message.minlength": "至少 {{min}} 个字符", + "com.affine.auth.set.password.message.maxlength": "至多 {{max}} 个字符", "com.affine.auth.set.password.page.success": "密码设置成功", "com.affine.auth.set.password.page.title": "设置您的 AFFiNE Cloud 密码", - "com.affine.auth.set.password.placeholder": "密码长度至少需要8个字符", + "com.affine.auth.set.password.placeholder": "密码长度至少需要 {{min}} 个字符", "com.affine.auth.set.password.placeholder.confirm": "确认密码", "com.affine.auth.set.password.save": "保存密码", "com.affine.auth.sign-out.confirm-modal.cancel": "取消", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index 45e570445a..65042065b0 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -329,7 +329,7 @@ "com.affine.auth.has.signed": "已登入!", "com.affine.auth.later": "之後", "com.affine.auth.open.affine": "打開 AFFiNE", - "com.affine.auth.page.sent.email.subtitle": "請設定由8-20位字母和數字組成的密碼", + "com.affine.auth.page.sent.email.subtitle": "請設定由 {{min}}-{{max}} 位字母和數字組成的密碼", "com.affine.auth.page.sent.email.title": "歡迎來到 AFFiNE Cloud,即將完成", "com.affine.auth.password": "密碼", "com.affine.auth.password.error": "無效的密碼", @@ -345,10 +345,12 @@ "com.affine.auth.sent.set.password.hint": "設定密碼連結已發送。", "com.affine.auth.set.email.save": "保存電子郵件地址", "com.affine.auth.set.password": "設定密碼", - "com.affine.auth.set.password.message": "請設定由8-20位字母和數字組成的密碼", + "com.affine.auth.set.password.message": "請設定由 {{min}}-{{max}} 位字母和數字組成的密碼", + "com.affine.auth.set.password.message.minlength": "至少 {{min}} 位字符", + "com.affine.auth.set.password.message.maxlength": "至多 {{max}} 位字符", "com.affine.auth.set.password.page.success": "成功設定密碼", "com.affine.auth.set.password.page.title": "重定您的 AFFiNE Cloud 密碼", - "com.affine.auth.set.password.placeholder": "設定至少包含 8 位字符的密碼", + "com.affine.auth.set.password.placeholder": "設定至少包含 {{min}} 位字符的密碼", "com.affine.auth.set.password.placeholder.confirm": "確認密碼", "com.affine.auth.set.password.save": "保存密碼", "com.affine.auth.sign.auth.code.error.hint": "驗證碼錯誤,請重試", diff --git a/packages/frontend/web/package.json b/packages/frontend/web/package.json index aa9503fa7b..4e3dc1ff21 100644 --- a/packages/frontend/web/package.json +++ b/packages/frontend/web/package.json @@ -16,13 +16,12 @@ "@juggle/resize-observer": "^3.4.0", "core-js": "^3.36.1", "intl-segmenter-polyfill-rs": "^0.1.7", - "mixpanel-browser": "^2.49.0", + "jotai": "^2.7.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@affine/cli": "workspace:*", - "@types/mixpanel-browser": "^2.49.0", "@types/react": "^18.2.60", "@types/react-dom": "^18.2.19", "typescript": "^5.3.3" diff --git a/packages/frontend/web/src/app.tsx b/packages/frontend/web/src/app.tsx index 67bab60ea2..a4076efbc4 100644 --- a/packages/frontend/web/src/app.tsx +++ b/packages/frontend/web/src/app.tsx @@ -12,12 +12,12 @@ import { performanceLogger, performanceRenderLogger, } from '@affine/core/shared'; +import { Telemetry } from '@affine/core/telemetry'; import createEmotionCache from '@affine/core/utils/create-emotion-cache'; import { configureWebServices } from '@affine/core/web'; import { createI18n, setUpLanguage } from '@affine/i18n'; import { CacheProvider } from '@emotion/react'; import { getCurrentStore, ServiceCollection } from '@toeverything/infra'; -import mixpanel from 'mixpanel-browser'; import type { PropsWithChildren, ReactElement } from 'react'; import { lazy, Suspense } from 'react'; import { RouterProvider } from 'react-router-dom'; @@ -62,13 +62,6 @@ const serviceProvider = services.provider(); export function App() { performanceRenderLogger.info('App'); - if (process.env.MIXPANEL_TOKEN) { - mixpanel.init(process.env.MIXPANEL_TOKEN || '', { - track_pageview: true, - persistence: 'localStorage', - }); - } - if (!languageLoadingPromise) { languageLoadingPromise = loadLanguage().catch(console.error); } @@ -79,6 +72,7 @@ export function App() { + diff --git a/yarn.lock b/yarn.lock index 9087911a3c..d7abd3e110 100644 --- a/yarn.lock +++ b/yarn.lock @@ -304,6 +304,7 @@ __metadata: vite: "npm:^5.1.4" vitest: "npm:1.4.0" yjs: "npm:^13.6.12" + zod: "npm:^3.22.4" peerDependencies: "@blocksuite/blocks": "*" "@blocksuite/global": "*" @@ -465,7 +466,6 @@ __metadata: "@emotion/react": "npm:^11.11.4" "@pengx17/electron-forge-maker-appimage": "npm:^1.1.1" "@toeverything/infra": "workspace:*" - "@types/mixpanel-browser": "npm:^2.49.0" "@types/uuid": "npm:^9.0.8" "@vitejs/plugin-react-swc": "npm:^3.6.0" async-call-rpc: "npm:^6.4.0" @@ -484,7 +484,6 @@ __metadata: jotai-devtools: "npm:^0.8.0" link-preview-js: "npm:^3.0.5" lodash-es: "npm:^4.17.21" - mixpanel-browser: "npm:^2.49.0" nanoid: "npm:^5.0.6" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" @@ -826,12 +825,11 @@ __metadata: "@affine/core": "workspace:*" "@affine/env": "workspace:*" "@juggle/resize-observer": "npm:^3.4.0" - "@types/mixpanel-browser": "npm:^2.49.0" "@types/react": "npm:^18.2.60" "@types/react-dom": "npm:^18.2.19" core-js: "npm:^3.36.1" intl-segmenter-polyfill-rs: "npm:^0.1.7" - mixpanel-browser: "npm:^2.49.0" + jotai: "npm:^2.7.1" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" typescript: "npm:^5.3.3" @@ -25241,9 +25239,9 @@ __metadata: languageName: node linkType: hard -"jotai@npm:^2.6.5": - version: 2.6.5 - resolution: "jotai@npm:2.6.5" +"jotai@npm:^2.6.5, jotai@npm:^2.7.1": + version: 2.7.1 + resolution: "jotai@npm:2.7.1" peerDependencies: "@types/react": ">=17.0.0" react: ">=17.0.0" @@ -25252,7 +25250,7 @@ __metadata: optional: true react: optional: true - checksum: 10/21461f2d97158607a72848c09ac6a46142f28b1cc6b1c9c026d9b48129627ca581307789670232e4ff3bede678ccc57f937edd5c40efaa9bce4a2daed43dcd26 + checksum: 10/7f735bb771885bee5dcd7e807458281d5422526837bbc52d9bcca9a093117245880794a868ff4e19fcd016064353d64e8d6f7b7485c081d678c51ac7fc459da8 languageName: node linkType: hard