From 44e17a2626263018d36556771fcbc307979148cf Mon Sep 17 00:00:00 2001 From: EYHN Date: Wed, 27 Nov 2024 15:55:31 +0800 Subject: [PATCH] feat(core): desktop multiple server support --- .github/deployment/self-host/compose.yaml | 2 +- .../server/src/core/config/resolver.ts | 6 + packages/backend/server/src/index.ts | 2 +- packages/backend/server/src/schema.gql | 1 + packages/common/env/src/workspace.ts | 7 - .../common/infra/src/framework/core/event.ts | 22 +- .../src/modules/feature-flag/constant.ts | 9 + .../global-context/entities/global-context.ts | 1 + .../workspace/__tests__/workspace.spec.ts | 3 +- .../src/modules/workspace/entities/list.ts | 45 ++-- .../src/modules/workspace/entities/profile.ts | 7 +- .../infra/src/modules/workspace/index.ts | 25 +- .../infra/src/modules/workspace/metadata.ts | 4 +- .../modules/workspace/providers/flavour.ts | 11 +- .../src/modules/workspace/services/destroy.ts | 8 +- .../src/modules/workspace/services/factory.ts | 11 +- .../modules/workspace/services/flavours.ts | 18 ++ .../src/modules/workspace/services/repo.ts | 10 +- .../modules/workspace/services/transform.ts | 5 +- .../modules/workspace/services/workspaces.ts | 6 +- .../workspace/testing/testing-provider.ts | 29 ++- packages/frontend/apps/electron/package.json | 2 + .../apps/electron/src/main/deep-link.ts | 2 + .../apps/electron/src/main/protocol.ts | 128 ++++++---- .../main/windows-manager/authentication.ts | 1 + .../components/auth-components/auth-input.tsx | 3 +- .../auth-components/modal-header.tsx | 4 +- .../components/auth-components/share.css.ts | 1 + .../affine/auth/after-sign-up-send-email.tsx | 121 ---------- .../affine/auth/ai-login-required.tsx | 11 +- .../core/src/components/affine/auth/index.tsx | 114 --------- .../core/src/components/affine/auth/oauth.tsx | 3 +- .../src/components/affine/auth/send-email.tsx | 221 ------------------ .../src/components/affine/auth/sign-in.tsx | 184 --------------- .../src/components/affine/auth/style.css.ts | 136 +---------- .../affine/auth/subscription-redirect.css.ts | 27 --- .../share-menu/share-menu.tsx | 3 +- .../share-menu/share-page.tsx | 8 +- .../core/src/components/atoms/index.ts | 37 --- .../block-suite-editor/ai/setup-provider.tsx | 13 +- .../block-suite-header/menu/index.tsx | 8 +- .../cloud/share-header-right-item/sign-in.tsx | 13 +- .../types/created-updated-by.tsx | 7 +- .../hooks/affine/use-enable-cloud.tsx | 9 +- ...se-register-blocksuite-editor-commands.tsx | 3 +- .../use-register-copy-link-commands.tsx | 3 +- .../components/hooks/affine/use-sign-out.ts | 22 +- .../providers/workspace-side-effects.tsx | 5 +- .../components/root-app-sidebar/user-info.tsx | 8 +- .../src/components/sign-in/add-selfhosted.tsx | 127 ++++++++++ .../use-captcha.tsx => sign-in/captcha.tsx} | 0 .../core/src/components/sign-in/index.tsx | 62 +++++ .../sign-in-with-email.tsx} | 99 +++++--- .../sign-in-with-password.tsx | 104 ++++----- .../core/src/components/sign-in/sign-in.tsx | 198 ++++++++++++++++ .../core/src/components/sign-in/style.css.ts | 97 ++++++++ .../frontend/core/src/components/top-tip.tsx | 12 +- .../user-with-workspace-list/index.tsx | 20 +- .../workspace-list/index.css.ts | 9 +- .../workspace-list/index.tsx | 169 ++++++++++---- .../workspace-card/index.tsx | 12 +- .../desktop/dialogs/change-password/index.tsx | 158 +++++++++++++ .../dialogs/create-workspace/index.tsx | 22 +- .../desktop/dialogs/import-template/index.tsx | 6 +- .../dialogs/import-workspace/index.tsx | 3 +- .../core/src/desktop/dialogs/index.tsx | 9 +- .../dialogs/setting/account-setting/index.tsx | 28 +-- .../plans/ai/actions/login.tsx | 13 +- .../general-setting/plans/plan-card.tsx | 12 +- .../dialogs/setting/setting-sidebar/index.tsx | 9 +- .../delete-leave-workspace/delete/index.tsx | 3 +- .../enable-cloud.tsx | 3 +- .../new-workspace-setting-detail/members.tsx | 3 +- .../new-workspace-setting-detail/sharing.tsx | 3 +- .../src/desktop/dialogs/sign-in/index.tsx | 25 ++ .../desktop/dialogs/verify-email/index.tsx | 145 ++++++++++++ .../core/src/desktop/pages/auth/sign-in.tsx | 42 ++-- .../core/src/desktop/pages/index/index.tsx | 13 +- .../src/desktop/pages/workspace/index.tsx | 27 ++- .../workspace/layouts/workspace-layout.tsx | 6 +- .../pages/workspace/share/share-header.tsx | 2 - .../pages/workspace/share/share-page.tsx | 3 +- .../sign-in/art-dark.inline.svg | 0 .../sign-in/art-light.inline.svg | 0 .../sign-in/background.css.ts | 0 .../sign-in/background.tsx | 0 .../src/mobile/components/sign-in/index.tsx | 17 ++ .../sign-in/layout.css.ts | 0 .../{views => components}/sign-in/layout.tsx | 0 .../components/workspace-selector/menu.tsx | 12 +- .../core/src/mobile/dialogs/index.tsx | 5 +- .../dialogs/setting/user-profile/index.tsx | 7 +- .../modal.tsx => dialogs/sign-in/index.tsx} | 34 ++- .../core/src/mobile/pages/root/index.tsx | 2 - .../core/src/mobile/pages/sign-in.tsx | 34 +-- .../detail/page-header-share-button.tsx | 3 +- .../src/mobile/pages/workspace/layout.tsx | 41 ++-- .../mobile/views/sign-in/mobile-sign-in.tsx | 11 - .../core/src/modules/cloud/constant.ts | 6 + .../core/src/modules/cloud/entities/server.ts | 2 +- .../src/modules/cloud/entities/session.ts | 2 +- .../modules/cloud/events/account-changed.ts | 7 + .../modules/cloud/events/account-logged-in.ts | 5 + .../cloud/events/account-logged-out.ts | 6 + .../cloud/events/server-initialized.ts | 5 + .../modules/cloud/events/server-started.ts | 3 + .../frontend/core/src/modules/cloud/index.ts | 18 +- .../core/src/modules/cloud/services/auth.ts | 27 +-- .../src/modules/cloud/services/captcha.ts | 22 +- .../src/modules/cloud/services/servers.ts | 63 ++++- .../modules/cloud/services/subscription.ts | 2 +- .../cloud/services/user-copilot-quota.ts | 2 +- .../modules/cloud/services/user-feature.ts | 2 +- .../src/modules/cloud/services/user-quota.ts | 2 +- .../src/modules/cloud/services/websocket.ts | 2 +- .../core/src/modules/cloud/stores/auth.ts | 14 +- .../src/modules/cloud/stores/server-config.ts | 14 +- .../src/modules/cloud/stores/server-list.ts | 8 +- .../desktop-api/service/desktop-api.ts | 22 +- .../core/src/modules/dialogs/constant.ts | 3 + .../src/modules/favorite/stores/favorite.ts | 3 +- .../import-template/services/import.ts | 3 +- .../permissions/entities/permission.ts | 6 +- .../share-doc/services/share-docs-list.ts | 3 +- .../core/src/modules/telemetry/index.ts | 7 +- .../modules/telemetry/services/telemetry.ts | 38 +-- .../modules/workspace-engine/impls/cloud.ts | 130 ++++++++--- .../modules/workspace-engine/impls/local.ts | 41 ++-- .../src/modules/workspace-engine/index.ts | 15 +- .../frontend/core/src/utils/first-app-data.ts | 5 +- .../fragments/credentials-requirement.gql | 1 + .../frontend/graphql/src/graphql/index.ts | 1 + packages/frontend/graphql/src/schema.ts | 4 + .../i18n/src/i18n-completenesses.json | 16 +- packages/frontend/i18n/src/resources/en.json | 7 + yarn.lock | 18 ++ 136 files changed, 1924 insertions(+), 1550 deletions(-) delete mode 100644 packages/common/env/src/workspace.ts create mode 100644 packages/common/infra/src/modules/workspace/services/flavours.ts delete mode 100644 packages/frontend/core/src/components/affine/auth/after-sign-up-send-email.tsx delete mode 100644 packages/frontend/core/src/components/affine/auth/index.tsx delete mode 100644 packages/frontend/core/src/components/affine/auth/send-email.tsx delete mode 100644 packages/frontend/core/src/components/affine/auth/sign-in.tsx delete mode 100644 packages/frontend/core/src/components/affine/auth/subscription-redirect.css.ts create mode 100644 packages/frontend/core/src/components/sign-in/add-selfhosted.tsx rename packages/frontend/core/src/components/{affine/auth/use-captcha.tsx => sign-in/captcha.tsx} (100%) create mode 100644 packages/frontend/core/src/components/sign-in/index.tsx rename packages/frontend/core/src/components/{affine/auth/after-sign-in-send-email.tsx => sign-in/sign-in-with-email.tsx} (64%) rename packages/frontend/core/src/components/{affine/auth => sign-in}/sign-in-with-password.tsx (67%) create mode 100644 packages/frontend/core/src/components/sign-in/sign-in.tsx create mode 100644 packages/frontend/core/src/components/sign-in/style.css.ts create mode 100644 packages/frontend/core/src/desktop/dialogs/change-password/index.tsx create mode 100644 packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx create mode 100644 packages/frontend/core/src/desktop/dialogs/verify-email/index.tsx rename packages/frontend/core/src/mobile/{views => components}/sign-in/art-dark.inline.svg (100%) rename packages/frontend/core/src/mobile/{views => components}/sign-in/art-light.inline.svg (100%) rename packages/frontend/core/src/mobile/{views => components}/sign-in/background.css.ts (100%) rename packages/frontend/core/src/mobile/{views => components}/sign-in/background.tsx (100%) create mode 100644 packages/frontend/core/src/mobile/components/sign-in/index.tsx rename packages/frontend/core/src/mobile/{views => components}/sign-in/layout.css.ts (100%) rename packages/frontend/core/src/mobile/{views => components}/sign-in/layout.tsx (100%) rename packages/frontend/core/src/mobile/{views/sign-in/modal.tsx => dialogs/sign-in/index.tsx} (54%) delete mode 100644 packages/frontend/core/src/mobile/views/sign-in/mobile-sign-in.tsx create mode 100644 packages/frontend/core/src/modules/cloud/events/account-changed.ts create mode 100644 packages/frontend/core/src/modules/cloud/events/account-logged-in.ts create mode 100644 packages/frontend/core/src/modules/cloud/events/account-logged-out.ts create mode 100644 packages/frontend/core/src/modules/cloud/events/server-initialized.ts create mode 100644 packages/frontend/core/src/modules/cloud/events/server-started.ts diff --git a/.github/deployment/self-host/compose.yaml b/.github/deployment/self-host/compose.yaml index dde7dc8ba09db..54143098034ea 100644 --- a/.github/deployment/self-host/compose.yaml +++ b/.github/deployment/self-host/compose.yaml @@ -5,7 +5,7 @@ services: command: ['sh', '-c', 'node ./scripts/self-host-predeploy && node ./dist/index.js'] ports: - - '3010:3010' + - '3011:3010' - '5555:5555' depends_on: redis: diff --git a/packages/backend/server/src/core/config/resolver.ts b/packages/backend/server/src/core/config/resolver.ts index d3e34629d3186..144c7319121ac 100644 --- a/packages/backend/server/src/core/config/resolver.ts +++ b/packages/backend/server/src/core/config/resolver.ts @@ -34,6 +34,9 @@ export class PasswordLimitsType { export class CredentialsRequirementType { @Field() password!: PasswordLimitsType; + + @Field() + email!: boolean; } registerEnumType(RuntimeConfigType, { @@ -108,11 +111,14 @@ export class ServerConfigResolver { 'auth/password.min': true, }); + const isMailerAvailable = this.config.mailer.host !== undefined; + return { password: { minLength: config['auth/password.min'], maxLength: config['auth/password.max'], }, + email: isMailerAvailable, }; } diff --git a/packages/backend/server/src/index.ts b/packages/backend/server/src/index.ts index 02e7d87d91c81..3bdf43534b5fb 100644 --- a/packages/backend/server/src/index.ts +++ b/packages/backend/server/src/index.ts @@ -8,7 +8,7 @@ import { createApp } from './app'; import { URLHelper } from './fundamentals'; const app = await createApp(); -const listeningHost = AFFiNE.deploy ? '0.0.0.0' : 'localhost'; +const listeningHost = '0.0.0.0'; await app.listen(AFFiNE.server.port, listeningHost); const url = app.get(URLHelper); diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 5d129eb89e1b4..3fd281d35626b 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -162,6 +162,7 @@ input CreateUserInput { } type CredentialsRequirementType { + email: Boolean! password: PasswordLimitsType! } diff --git a/packages/common/env/src/workspace.ts b/packages/common/env/src/workspace.ts deleted file mode 100644 index 55cee5180e963..0000000000000 --- a/packages/common/env/src/workspace.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum WorkspaceFlavour { - /** - * New AFFiNE Cloud Workspace using Nest.js Server. - */ - AFFINE_CLOUD = 'affine-cloud', - LOCAL = 'local', -} diff --git a/packages/common/infra/src/framework/core/event.ts b/packages/common/infra/src/framework/core/event.ts index cf1b824272a37..6fe5b6f390115 100644 --- a/packages/common/infra/src/framework/core/event.ts +++ b/packages/common/infra/src/framework/core/event.ts @@ -33,7 +33,7 @@ export class EventBus { }); for (const handler of handlers.values()) { - this.on(handler.event.id, handler.handler); + this.on(handler.event, handler.handler); } } @@ -41,23 +41,25 @@ export class EventBus { return this.parent?.root ?? this; } - on(id: string, listener: (event: FrameworkEvent) => void) { - if (!this.listeners[id]) { - this.listeners[id] = []; + on(event: FrameworkEvent, listener: (event: T) => void) { + if (!this.listeners[event.id]) { + this.listeners[event.id] = []; } - this.listeners[id].push(listener); - const off = this.parent?.on(id, listener); + this.listeners[event.id].push(listener); + const off = this.parent?.on(event, listener); return () => { - this.off(id, listener); + this.off(event, listener); off?.(); }; } - off(id: string, listener: (event: FrameworkEvent) => void) { - if (!this.listeners[id]) { + off(event: FrameworkEvent, listener: (event: T) => void) { + if (!this.listeners[event.id]) { return; } - this.listeners[id] = this.listeners[id].filter(l => l !== listener); + this.listeners[event.id] = this.listeners[event.id].filter( + l => l !== listener + ); } emit(event: FrameworkEvent, payload: T) { diff --git a/packages/common/infra/src/modules/feature-flag/constant.ts b/packages/common/infra/src/modules/feature-flag/constant.ts index 07fb240bba8fb..1d857464e7549 100644 --- a/packages/common/infra/src/modules/feature-flag/constant.ts +++ b/packages/common/infra/src/modules/feature-flag/constant.ts @@ -206,6 +206,15 @@ export const AFFINE_FLAGS = { configurable: false, defaultState: isMobile, }, + enable_multiple_cloud_servers: { + category: 'affine', + displayName: + 'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.name', + description: + 'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.description', + configurable: isDesktopEnvironment, + defaultState: false, + }, } satisfies { [key in string]: FlagInfo }; export type AFFINE_FLAGS = typeof AFFINE_FLAGS; diff --git a/packages/common/infra/src/modules/global-context/entities/global-context.ts b/packages/common/infra/src/modules/global-context/entities/global-context.ts index 37f0bedfd9718..3377df881195a 100644 --- a/packages/common/infra/src/modules/global-context/entities/global-context.ts +++ b/packages/common/infra/src/modules/global-context/entities/global-context.ts @@ -8,6 +8,7 @@ export class GlobalContext extends Entity { memento = new MemoryMemento(); workspaceId = this.define('workspaceId'); + workspaceFlavour = this.define('workspaceFlavour'); /** * is in doc page diff --git a/packages/common/infra/src/modules/workspace/__tests__/workspace.spec.ts b/packages/common/infra/src/modules/workspace/__tests__/workspace.spec.ts index 44ceeb92e66b4..d0f9699211e46 100644 --- a/packages/common/infra/src/modules/workspace/__tests__/workspace.spec.ts +++ b/packages/common/infra/src/modules/workspace/__tests__/workspace.spec.ts @@ -1,4 +1,3 @@ -import { WorkspaceFlavour } from '@affine/env/workspace'; import { describe, expect, test } from 'vitest'; import { Framework } from '../../../framework'; @@ -22,7 +21,7 @@ describe('Workspace System', () => { expect(workspaceService.list.workspaces$.value.length).toBe(0); const workspace = workspaceService.open({ - metadata: await workspaceService.create(WorkspaceFlavour.LOCAL), + metadata: await workspaceService.create('local'), }); expect(workspace.workspace).toBeInstanceOf(Workspace); diff --git a/packages/common/infra/src/modules/workspace/entities/list.ts b/packages/common/infra/src/modules/workspace/entities/list.ts index e965262998865..54fda37b0f693 100644 --- a/packages/common/infra/src/modules/workspace/entities/list.ts +++ b/packages/common/infra/src/modules/workspace/entities/list.ts @@ -1,21 +1,32 @@ +import { combineLatest, map, of, switchMap } from 'rxjs'; + import { Entity } from '../../../framework'; import { LiveData } from '../../../livedata'; -import type { WorkspaceFlavourProvider } from '../providers/flavour'; +import type { WorkspaceMetadata } from '../metadata'; +import type { WorkspaceFlavoursService } from '../services/flavours'; export class WorkspaceList extends Entity { - workspaces$ = new LiveData(this.providers.map(p => p.workspaces$)) - .map(v => { - return v; - }) - .flat() - .map(workspaces => { - return workspaces.flat(); - }); - isRevalidating$ = new LiveData( - this.providers.map(p => p.isRevalidating$ ?? new LiveData(false)) - ) - .flat() - .map(isLoadings => isLoadings.some(isLoading => isLoading)); + workspaces$ = LiveData.from( + this.flavoursService.flavours$.pipe( + switchMap(flavours => + combineLatest(flavours.map(flavour => flavour.workspaces$)).pipe( + map(workspaces => workspaces.flat()) + ) + ) + ), + [] + ); + + isRevalidating$ = LiveData.from( + this.flavoursService.flavours$.pipe( + switchMap(flavours => + combineLatest( + flavours.map(flavour => flavour.isRevalidating$ ?? of(false)) + ).pipe(map(isLoadings => isLoadings.some(isLoading => isLoading))) + ) + ), + false + ); workspace$(id: string) { return this.workspaces$.map(workspaces => @@ -23,12 +34,14 @@ export class WorkspaceList extends Entity { ); } - constructor(private readonly providers: WorkspaceFlavourProvider[]) { + constructor(private readonly flavoursService: WorkspaceFlavoursService) { super(); } revalidate() { - this.providers.forEach(provider => provider.revalidate?.()); + this.flavoursService.flavours$.value.forEach(provider => { + provider.revalidate?.(); + }); } waitForRevalidation(signal?: AbortSignal) { diff --git a/packages/common/infra/src/modules/workspace/entities/profile.ts b/packages/common/infra/src/modules/workspace/entities/profile.ts index ab23001ff0973..005b9251907f6 100644 --- a/packages/common/infra/src/modules/workspace/entities/profile.ts +++ b/packages/common/infra/src/modules/workspace/entities/profile.ts @@ -12,6 +12,7 @@ import { } from '../../../livedata'; import type { WorkspaceMetadata } from '../metadata'; import type { WorkspaceFlavourProvider } from '../providers/flavour'; +import type { WorkspaceFlavoursService } from '../services/flavours'; import type { WorkspaceProfileCacheStore } from '../stores/profile-cache'; import type { Workspace } from './workspace'; @@ -47,12 +48,14 @@ export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> { constructor( private readonly cache: WorkspaceProfileCacheStore, - providers: WorkspaceFlavourProvider[] + flavoursService: WorkspaceFlavoursService ) { super(); this.provider = - providers.find(p => p.flavour === this.props.metadata.flavour) ?? null; + flavoursService.flavours$.value.find( + p => p.flavour === this.props.metadata.flavour + ) ?? null; } private setProfile(info: WorkspaceProfileInfo) { diff --git a/packages/common/infra/src/modules/workspace/index.ts b/packages/common/infra/src/modules/workspace/index.ts index 8568d47ab7787..763909270591a 100644 --- a/packages/common/infra/src/modules/workspace/index.ts +++ b/packages/common/infra/src/modules/workspace/index.ts @@ -5,7 +5,8 @@ export { getAFFiNEWorkspaceSchema } from './global-schema'; export type { WorkspaceMetadata } from './metadata'; export type { WorkspaceOpenOptions } from './open-options'; export type { WorkspaceEngineProvider } from './providers/flavour'; -export { WorkspaceFlavourProvider } from './providers/flavour'; +export type { WorkspaceFlavourProvider } from './providers/flavour'; +export { WorkspaceFlavoursProvider } from './providers/flavour'; export { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage'; export { WorkspaceScope } from './scopes/workspace'; export { WorkspaceService } from './services/workspace'; @@ -21,12 +22,13 @@ import { WorkspaceLocalCacheImpl, WorkspaceLocalStateImpl, } from './impls/storage'; -import { WorkspaceFlavourProvider } from './providers/flavour'; +import { WorkspaceFlavoursProvider } from './providers/flavour'; import { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage'; import { WorkspaceScope } from './scopes/workspace'; import { WorkspaceDestroyService } from './services/destroy'; import { WorkspaceEngineService } from './services/engine'; import { WorkspaceFactoryService } from './services/factory'; +import { WorkspaceFlavoursService } from './services/flavours'; import { WorkspaceListService } from './services/list'; import { WorkspaceProfileService } from './services/profile'; import { WorkspaceRepositoryService } from './services/repo'; @@ -34,12 +36,12 @@ import { WorkspaceTransformService } from './services/transform'; import { WorkspaceService } from './services/workspace'; import { WorkspacesService } from './services/workspaces'; import { WorkspaceProfileCacheStore } from './stores/profile-cache'; -import { TestingWorkspaceLocalProvider } from './testing/testing-provider'; +import { TestingWorkspaceFlavoursProvider } from './testing/testing-provider'; export function configureWorkspaceModule(framework: Framework) { framework .service(WorkspacesService, [ - [WorkspaceFlavourProvider], + WorkspaceFlavoursService, WorkspaceListService, WorkspaceProfileService, WorkspaceTransformService, @@ -47,22 +49,23 @@ export function configureWorkspaceModule(framework: Framework) { WorkspaceFactoryService, WorkspaceDestroyService, ]) - .service(WorkspaceDestroyService, [[WorkspaceFlavourProvider]]) + .service(WorkspaceFlavoursService, [[WorkspaceFlavoursProvider]]) + .service(WorkspaceDestroyService, [WorkspaceFlavoursService]) .service(WorkspaceListService) - .entity(WorkspaceList, [[WorkspaceFlavourProvider]]) + .entity(WorkspaceList, [WorkspaceFlavoursService]) .service(WorkspaceProfileService) .store(WorkspaceProfileCacheStore, [GlobalCache]) .entity(WorkspaceProfile, [ WorkspaceProfileCacheStore, - [WorkspaceFlavourProvider], + WorkspaceFlavoursService, ]) - .service(WorkspaceFactoryService, [[WorkspaceFlavourProvider]]) + .service(WorkspaceFactoryService, [WorkspaceFlavoursService]) .service(WorkspaceTransformService, [ WorkspaceFactoryService, WorkspaceDestroyService, ]) .service(WorkspaceRepositoryService, [ - [WorkspaceFlavourProvider], + WorkspaceFlavoursService, WorkspaceProfileService, ]) .scope(WorkspaceScope) @@ -82,8 +85,8 @@ export function configureWorkspaceModule(framework: Framework) { export function configureTestingWorkspaceProvider(framework: Framework) { framework.impl( - WorkspaceFlavourProvider('LOCAL'), - TestingWorkspaceLocalProvider, + WorkspaceFlavoursProvider('LOCAL'), + TestingWorkspaceFlavoursProvider, [GlobalState] ); } diff --git a/packages/common/infra/src/modules/workspace/metadata.ts b/packages/common/infra/src/modules/workspace/metadata.ts index 990d4c7ffe044..352ec1312bf09 100644 --- a/packages/common/infra/src/modules/workspace/metadata.ts +++ b/packages/common/infra/src/modules/workspace/metadata.ts @@ -1,7 +1,5 @@ -import type { WorkspaceFlavour } from '@affine/env/workspace'; - export type WorkspaceMetadata = { id: string; - flavour: WorkspaceFlavour; + flavour: string; initialized?: boolean; }; diff --git a/packages/common/infra/src/modules/workspace/providers/flavour.ts b/packages/common/infra/src/modules/workspace/providers/flavour.ts index 1c1a0d3bdc3b1..ce2a24fc3ab7a 100644 --- a/packages/common/infra/src/modules/workspace/providers/flavour.ts +++ b/packages/common/infra/src/modules/workspace/providers/flavour.ts @@ -1,4 +1,3 @@ -import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { DocCollection } from '@blocksuite/affine/store'; import { createIdentifier } from '../../../framework'; @@ -22,7 +21,7 @@ export interface WorkspaceEngineProvider { } export interface WorkspaceFlavourProvider { - flavour: WorkspaceFlavour; + flavour: string; deleteWorkspace(id: string): Promise; @@ -60,5 +59,9 @@ export interface WorkspaceFlavourProvider { onWorkspaceInitialized?(workspace: Workspace): void; } -export const WorkspaceFlavourProvider = - createIdentifier('WorkspaceFlavourProvider'); +export interface WorkspaceFlavoursProvider { + workspaceFlavours$: LiveData; +} + +export const WorkspaceFlavoursProvider = + createIdentifier('WorkspaceFlavoursProvider'); diff --git a/packages/common/infra/src/modules/workspace/services/destroy.ts b/packages/common/infra/src/modules/workspace/services/destroy.ts index 90639a3283f1b..71a090e5416b7 100644 --- a/packages/common/infra/src/modules/workspace/services/destroy.ts +++ b/packages/common/infra/src/modules/workspace/services/destroy.ts @@ -1,14 +1,16 @@ import { Service } from '../../../framework'; import type { WorkspaceMetadata } from '../metadata'; -import type { WorkspaceFlavourProvider } from '../providers/flavour'; +import type { WorkspaceFlavoursService } from './flavours'; export class WorkspaceDestroyService extends Service { - constructor(private readonly providers: WorkspaceFlavourProvider[]) { + constructor(private readonly flavoursService: WorkspaceFlavoursService) { super(); } deleteWorkspace = async (metadata: WorkspaceMetadata) => { - const provider = this.providers.find(p => p.flavour === metadata.flavour); + const provider = this.flavoursService.flavours$.value.find( + p => p.flavour === metadata.flavour + ); if (!provider) { throw new Error(`Unknown workspace flavour: ${metadata.flavour}`); } diff --git a/packages/common/infra/src/modules/workspace/services/factory.ts b/packages/common/infra/src/modules/workspace/services/factory.ts index 5b87046495edd..2442e227dc7eb 100644 --- a/packages/common/infra/src/modules/workspace/services/factory.ts +++ b/packages/common/infra/src/modules/workspace/services/factory.ts @@ -1,12 +1,11 @@ -import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { DocCollection } from '@blocksuite/affine/store'; import { Service } from '../../../framework'; import type { BlobStorage, DocStorage } from '../../../sync'; -import type { WorkspaceFlavourProvider } from '../providers/flavour'; +import type { WorkspaceFlavoursService } from './flavours'; export class WorkspaceFactoryService extends Service { - constructor(private readonly providers: WorkspaceFlavourProvider[]) { + constructor(private readonly flavoursService: WorkspaceFlavoursService) { super(); } @@ -17,14 +16,16 @@ export class WorkspaceFactoryService extends Service { * @returns workspace id */ create = async ( - flavour: WorkspaceFlavour, + flavour: string, initial: ( docCollection: DocCollection, blobStorage: BlobStorage, docStorage: DocStorage ) => Promise = () => Promise.resolve() ) => { - const provider = this.providers.find(x => x.flavour === flavour); + const provider = this.flavoursService.flavours$.value.find( + x => x.flavour === flavour + ); if (!provider) { throw new Error(`Unknown workspace flavour: ${flavour}`); } diff --git a/packages/common/infra/src/modules/workspace/services/flavours.ts b/packages/common/infra/src/modules/workspace/services/flavours.ts new file mode 100644 index 0000000000000..c248eb36e0630 --- /dev/null +++ b/packages/common/infra/src/modules/workspace/services/flavours.ts @@ -0,0 +1,18 @@ +import { combineLatest, map } from 'rxjs'; + +import { Service } from '../../../framework'; +import { LiveData } from '../../../livedata'; +import type { WorkspaceFlavoursProvider } from '../providers/flavour'; + +export class WorkspaceFlavoursService extends Service { + constructor(private readonly providers: WorkspaceFlavoursProvider[]) { + super(); + } + + flavours$ = LiveData.from( + combineLatest(this.providers.map(p => p.workspaceFlavours$)).pipe( + map(flavours => flavours.flat()) + ), + [] + ); +} diff --git a/packages/common/infra/src/modules/workspace/services/repo.ts b/packages/common/infra/src/modules/workspace/services/repo.ts index 53aa6ca2e9f3b..5ff50fcf05678 100644 --- a/packages/common/infra/src/modules/workspace/services/repo.ts +++ b/packages/common/infra/src/modules/workspace/services/repo.ts @@ -5,11 +5,9 @@ import { ObjectPool } from '../../../utils'; import type { Workspace } from '../entities/workspace'; import { WorkspaceInitialized } from '../events'; import type { WorkspaceOpenOptions } from '../open-options'; -import type { - WorkspaceEngineProvider, - WorkspaceFlavourProvider, -} from '../providers/flavour'; +import type { WorkspaceEngineProvider } from '../providers/flavour'; import { WorkspaceScope } from '../scopes/workspace'; +import type { WorkspaceFlavoursService } from './flavours'; import type { WorkspaceProfileService } from './profile'; import { WorkspaceService } from './workspace'; @@ -17,7 +15,7 @@ const logger = new DebugLogger('affine:workspace-repository'); export class WorkspaceRepositoryService extends Service { constructor( - private readonly providers: WorkspaceFlavourProvider[], + private readonly flavoursService: WorkspaceFlavoursService, private readonly profileRepo: WorkspaceProfileService ) { super(); @@ -83,7 +81,7 @@ export class WorkspaceRepositoryService extends Service { logger.info( `open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} ` ); - const flavourProvider = this.providers.find( + const flavourProvider = this.flavoursService.flavours$.value.find( p => p.flavour === openOptions.metadata.flavour ); const provider = diff --git a/packages/common/infra/src/modules/workspace/services/transform.ts b/packages/common/infra/src/modules/workspace/services/transform.ts index 06404e00ac4eb..b40671f51a063 100644 --- a/packages/common/infra/src/modules/workspace/services/transform.ts +++ b/packages/common/infra/src/modules/workspace/services/transform.ts @@ -1,4 +1,3 @@ -import { WorkspaceFlavour } from '@affine/env/workspace'; import { assertEquals } from '@blocksuite/affine/global/utils'; import { applyUpdate } from 'yjs'; @@ -26,12 +25,12 @@ export class WorkspaceTransformService extends Service { local: Workspace, accountId: string ): Promise => { - assertEquals(local.flavour, WorkspaceFlavour.LOCAL); + assertEquals(local.flavour, 'local'); const localDocStorage = local.engine.doc.storage.behavior; const newMetadata = await this.factory.create( - WorkspaceFlavour.AFFINE_CLOUD, + 'affine-cloud', async (docCollection, blobStorage, docStorage) => { const rootDocBinary = await localDocStorage.doc.get( local.docCollection.doc.guid diff --git a/packages/common/infra/src/modules/workspace/services/workspaces.ts b/packages/common/infra/src/modules/workspace/services/workspaces.ts index 56cf35cc1f6f8..455982105aa60 100644 --- a/packages/common/infra/src/modules/workspace/services/workspaces.ts +++ b/packages/common/infra/src/modules/workspace/services/workspaces.ts @@ -1,8 +1,8 @@ import { Service } from '../../../framework'; import type { WorkspaceMetadata } from '..'; -import type { WorkspaceFlavourProvider } from '../providers/flavour'; import type { WorkspaceDestroyService } from './destroy'; import type { WorkspaceFactoryService } from './factory'; +import type { WorkspaceFlavoursService } from './flavours'; import type { WorkspaceListService } from './list'; import type { WorkspaceProfileService } from './profile'; import type { WorkspaceRepositoryService } from './repo'; @@ -14,7 +14,7 @@ export class WorkspacesService extends Service { } constructor( - private readonly providers: WorkspaceFlavourProvider[], + private readonly flavoursService: WorkspaceFlavoursService, private readonly listService: WorkspaceListService, private readonly profileRepo: WorkspaceProfileService, private readonly transform: WorkspaceTransformService, @@ -46,7 +46,7 @@ export class WorkspacesService extends Service { } async getWorkspaceBlob(meta: WorkspaceMetadata, blob: string) { - return await this.providers + return await this.flavoursService.flavours$.value .find(x => x.flavour === meta.flavour) ?.getWorkspaceBlob(meta.id, blob); } diff --git a/packages/common/infra/src/modules/workspace/testing/testing-provider.ts b/packages/common/infra/src/modules/workspace/testing/testing-provider.ts index 11468860a4a33..8ae30dcbc6956 100644 --- a/packages/common/infra/src/modules/workspace/testing/testing-provider.ts +++ b/packages/common/infra/src/modules/workspace/testing/testing-provider.ts @@ -1,4 +1,3 @@ -import { WorkspaceFlavour } from '@affine/env/workspace'; import { DocCollection, nanoid } from '@blocksuite/affine/store'; import { map } from 'rxjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; @@ -19,21 +18,17 @@ import type { WorkspaceMetadata } from '../metadata'; import type { WorkspaceEngineProvider, WorkspaceFlavourProvider, + WorkspaceFlavoursProvider, } from '../providers/flavour'; -export class TestingWorkspaceLocalProvider - extends Service - implements WorkspaceFlavourProvider -{ - flavour: WorkspaceFlavour = WorkspaceFlavour.LOCAL; +class TestingWorkspaceLocalProvider implements WorkspaceFlavourProvider { + flavour = 'local'; store = wrapMemento(this.globalStore, 'testing/'); workspaceListStore = wrapMemento(this.store, 'workspaces/'); docStorage = new MemoryDocStorage(wrapMemento(this.store, 'docs/')); - constructor(private readonly globalStore: GlobalState) { - super(); - } + constructor(private readonly globalStore: GlobalState) {} async deleteWorkspace(id: string): Promise { const list = this.workspaceListStore.get('list') ?? []; @@ -48,7 +43,7 @@ export class TestingWorkspaceLocalProvider ) => Promise ): Promise { const id = nanoid(); - const meta = { id, flavour: WorkspaceFlavour.LOCAL }; + const meta = { id, flavour: 'local' }; const blobStorage = new MemoryBlobStorage( wrapMemento(this.store, id + '/blobs/') @@ -75,7 +70,7 @@ export class TestingWorkspaceLocalProvider const list = this.workspaceListStore.get('list') ?? []; this.workspaceListStore.set('list', [...list, meta]); - return { id, flavour: WorkspaceFlavour.LOCAL }; + return { id, flavour: 'local' }; } workspaces$ = LiveData.from( this.workspaceListStore @@ -132,3 +127,15 @@ export class TestingWorkspaceLocalProvider }; } } + +export class TestingWorkspaceFlavoursProvider + extends Service + implements WorkspaceFlavoursProvider +{ + constructor(private readonly globalStore: GlobalState) { + super(); + } + workspaceFlavours$ = new LiveData([ + new TestingWorkspaceLocalProvider(this.globalStore), + ]); +} diff --git a/packages/frontend/apps/electron/package.json b/packages/frontend/apps/electron/package.json index 49a014e8c8d61..7cc77b4d75017 100644 --- a/packages/frontend/apps/electron/package.json +++ b/packages/frontend/apps/electron/package.json @@ -45,6 +45,7 @@ "@sentry/esbuild-plugin": "^2.16.1", "@sentry/react": "^8.0.0", "@toeverything/infra": "workspace:*", + "@types/set-cookie-parser": "^2.4.10", "@types/uuid": "^10.0.0", "@vitejs/plugin-react-swc": "^3.6.0", "builder-util-runtime": "^9.2.5-alpha.2", @@ -74,6 +75,7 @@ "electron-updater": "^6.2.1", "link-preview-js": "^3.0.5", "next-themes": "^0.4.0", + "set-cookie-parser": "^2.7.1", "yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch" }, "build": { diff --git a/packages/frontend/apps/electron/src/main/deep-link.ts b/packages/frontend/apps/electron/src/main/deep-link.ts index 16c88bd27cd05..128e368dfda38 100644 --- a/packages/frontend/apps/electron/src/main/deep-link.ts +++ b/packages/frontend/apps/electron/src/main/deep-link.ts @@ -84,6 +84,7 @@ async function handleAffineUrl(url: string) { if (urlObj.hostname === 'authentication') { const method = urlObj.searchParams.get('method'); const payload = JSON.parse(urlObj.searchParams.get('payload') ?? 'false'); + const server = urlObj.searchParams.get('server') || undefined; if ( !method || @@ -97,6 +98,7 @@ async function handleAffineUrl(url: string) { uiSubjects.authenticationRequest$.next({ method, payload, + server, }); } else if ( urlObj.searchParams.get('new-tab') && diff --git a/packages/frontend/apps/electron/src/main/protocol.ts b/packages/frontend/apps/electron/src/main/protocol.ts index 85101a1089650..70c6a0fd4c15e 100644 --- a/packages/frontend/apps/electron/src/main/protocol.ts +++ b/packages/frontend/apps/electron/src/main/protocol.ts @@ -1,11 +1,11 @@ import { join } from 'node:path'; import { net, protocol, session } from 'electron'; +import cookieParser from 'set-cookie-parser'; import { CLOUD_BASE_URL } from './config'; import { logger } from './logger'; import { isOfflineModeEnabled } from './utils'; -import { getCookies } from './windows-manager'; protocol.registerSchemesAsPrivileged([ { @@ -83,31 +83,72 @@ export function registerProtocol() { return handleFileRequest(request); }); - // todo(@pengx17): remove this session.defaultSession.webRequest.onHeadersReceived( (responseDetails, callback) => { const { responseHeaders } = responseDetails; - if (responseHeaders) { - // replace SameSite=Lax with SameSite=None - const originalCookie = - responseHeaders['set-cookie'] || responseHeaders['Set-Cookie']; - - if (originalCookie) { - delete responseHeaders['set-cookie']; - delete responseHeaders['Set-Cookie']; - responseHeaders['Set-Cookie'] = originalCookie.map(cookie => { - let newCookie = cookie.replace(/SameSite=Lax/gi, 'SameSite=None'); - - // if the cookie is not secure, set it to secure - if (!newCookie.includes('Secure')) { - newCookie = newCookie + '; Secure'; + (async () => { + if (responseHeaders) { + const originalCookie = + responseHeaders['set-cookie'] || responseHeaders['Set-Cookie']; + + if (originalCookie) { + // save the cookies, to support third party cookies + for (const cookies of originalCookie) { + const parsedCookies = cookieParser.parse(cookies); + for (const parsedCookie of parsedCookies) { + console.log('set cookie', { + url: responseDetails.url, + domain: parsedCookie.domain, + expirationDate: parsedCookie.expires?.getTime(), + httpOnly: parsedCookie.httpOnly, + secure: parsedCookie.secure, + value: parsedCookie.value, + name: parsedCookie.name, + path: parsedCookie.path, + sameSite: parsedCookie.sameSite?.toLowerCase() as + | 'unspecified' + | 'no_restriction' + | 'lax' + | 'strict' + | undefined, + }); + if (!parsedCookie.value) { + await session.defaultSession.cookies.remove( + responseDetails.url, + parsedCookie.name + ); + } else { + await session.defaultSession.cookies.set({ + url: responseDetails.url, + domain: parsedCookie.domain, + expirationDate: parsedCookie.expires?.getTime(), + httpOnly: parsedCookie.httpOnly, + secure: parsedCookie.secure, + value: parsedCookie.value, + name: parsedCookie.name, + path: parsedCookie.path, + sameSite: parsedCookie.sameSite?.toLowerCase() as + | 'unspecified' + | 'no_restriction' + | 'lax' + | 'strict' + | undefined, + }); + } + } } - return newCookie; - }); - } - } + } - callback({ responseHeaders }); + responseHeaders['Access-Control-Allow-Origin'] = ['*']; + responseHeaders['Access-Control-Allow-Headers'] = ['*']; + } + })() + .catch(err => { + logger.error('error handling headers received', err); + }) + .finally(() => { + callback({ responseHeaders }); + }); } ); @@ -117,9 +158,6 @@ export function registerProtocol() { const protocol = url.protocol; const origin = url.origin; - const sameSite = - url.host === new URL(CLOUD_BASE_URL).host || protocol === 'file:'; - // offline whitelist // 1. do not block non-api request for http://localhost || file:// (local dev assets) // 2. do not block devtools @@ -148,22 +186,34 @@ export function registerProtocol() { return; } - // session cookies are set to file:// on production - // if sending request to the cloud, attach the session cookie (to affine cloud server) - if (isNetworkResource(pathname) && sameSite) { - const cookie = getCookies(); - if (cookie) { - const cookieString = cookie.map(c => `${c.name}=${c.value}`).join('; '); + (async () => { + // session cookies are set to file:// on production + // if sending request to the cloud, attach the session cookie (to affine cloud server) + if ( + url.protocol === 'http:' || + url.protocol === 'https:' || + url.protocol === 'ws:' || + url.protocol === 'wss:' + ) { + const cookies = await session.defaultSession.cookies.get({ + url: details.url, + }); + console.log('get cookie', details.url, cookies); + + const cookieString = cookies + .map(c => `${c.name}=${c.value}`) + .join('; '); details.requestHeaders['cookie'] = cookieString; } - - // add the referer and origin headers - details.requestHeaders['referer'] ??= CLOUD_BASE_URL; - details.requestHeaders['origin'] ??= CLOUD_BASE_URL; - } - callback({ - cancel: false, - requestHeaders: details.requestHeaders, - }); + })() + .catch(err => { + logger.error('error handling before send headers', err); + }) + .finally(() => { + callback({ + cancel: false, + requestHeaders: details.requestHeaders, + }); + }); }); } diff --git a/packages/frontend/apps/electron/src/main/windows-manager/authentication.ts b/packages/frontend/apps/electron/src/main/windows-manager/authentication.ts index 7a70e914521d4..572ce48df3b55 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/authentication.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/authentication.ts @@ -1,4 +1,5 @@ export interface AuthenticationRequest { method: 'magic-link' | 'oauth'; payload: Record; + server?: string; } diff --git a/packages/frontend/component/src/components/auth-components/auth-input.tsx b/packages/frontend/component/src/components/auth-components/auth-input.tsx index 3732364828305..7e2ae52d8efe5 100644 --- a/packages/frontend/component/src/components/auth-components/auth-input.tsx +++ b/packages/frontend/component/src/components/auth-components/auth-input.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx'; +import type { ReactNode } from 'react'; import type { InputProps } from '../../ui/input'; import { Input } from '../../ui/input'; @@ -6,7 +7,7 @@ import * as styles from './share.css'; export type AuthInputProps = InputProps & { label?: string; error?: boolean; - errorHint?: string; + errorHint?: ReactNode; withoutHint?: boolean; onEnter?: () => void; }; diff --git a/packages/frontend/component/src/components/auth-components/modal-header.tsx b/packages/frontend/component/src/components/auth-components/modal-header.tsx index 2cf9be056cf7f..a69bec50c6c8c 100644 --- a/packages/frontend/component/src/components/auth-components/modal-header.tsx +++ b/packages/frontend/component/src/components/auth-components/modal-header.tsx @@ -4,12 +4,12 @@ import type { FC } from 'react'; import { modalHeaderWrapper } from './share.css'; export const ModalHeader: FC<{ title: string; - subTitle: string; + subTitle?: string; }> = ({ title, subTitle }) => { return (

- + {title === 'AFFiNE Cloud' && } {title}

{subTitle}

diff --git a/packages/frontend/component/src/components/auth-components/share.css.ts b/packages/frontend/component/src/components/auth-components/share.css.ts index 0a4ed1bbd71af..5337caef69963 100644 --- a/packages/frontend/component/src/components/auth-components/share.css.ts +++ b/packages/frontend/component/src/components/auth-components/share.css.ts @@ -42,6 +42,7 @@ export const formHint = style({ fontSize: cssVar('fontSm'), position: 'absolute', bottom: '4px', + height: '22px', left: 0, lineHeight: '22px', selectors: { diff --git a/packages/frontend/core/src/components/affine/auth/after-sign-up-send-email.tsx b/packages/frontend/core/src/components/affine/auth/after-sign-up-send-email.tsx deleted file mode 100644 index 7509b70bea758..0000000000000 --- a/packages/frontend/core/src/components/affine/auth/after-sign-up-send-email.tsx +++ /dev/null @@ -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 ( - <> - - - }} - /> - {t['com.affine.auth.sign.sent.email.message.sent-tips.sign-up']()} - - -
- {resendCountDown <= 0 ? ( - <> - - - - ) : ( -
-
- {t['com.affine.auth.sent']()} -
- -
- )} -
- -
- {t['com.affine.auth.sign.auth.code.message']()} -
- - { - setAuthData({ state: 'signIn' }); - }, [setAuthData])} - /> - - ); -}; diff --git a/packages/frontend/core/src/components/affine/auth/ai-login-required.tsx b/packages/frontend/core/src/components/affine/auth/ai-login-required.tsx index 0748a9cc2cbb2..57d3648ec2804 100644 --- a/packages/frontend/core/src/components/affine/auth/ai-login-required.tsx +++ b/packages/frontend/core/src/components/affine/auth/ai-login-required.tsx @@ -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) { diff --git a/packages/frontend/core/src/components/affine/auth/index.tsx b/packages/frontend/core/src/components/affine/auth/index.tsx deleted file mode 100644 index b6ec7edfe2ae5..0000000000000 --- a/packages/frontend/core/src/components/affine/auth/index.tsx +++ /dev/null @@ -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 = Extract< - AuthAtomData, - { state: T } ->; - -// return field in B that is not in A -type Difference< - A extends Record, - B extends Record, -> = Pick>; - -export type AuthPanelProps = { - setAuthData: ( - updates: { state: T } & Difference, AuthAtomType> - ) => void; - onSkip?: () => void; - redirectUrl?: string; -} & Extract; - -const config: { - [k in AuthAtomData['state']]: FC>; -} = { - 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 ( - - - - ); -} - -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) => { - // @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 ; -} diff --git a/packages/frontend/core/src/components/affine/auth/oauth.tsx b/packages/frontend/core/src/components/affine/auth/oauth.tsx index 6c2008d4acbcd..371562c46f318 100644 --- a/packages/frontend/core/src/components/affine/auth/oauth.tsx +++ b/packages/frontend/core/src/components/affine/auth/oauth.tsx @@ -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 ; + return null; } return oauthProviders?.map(provider => ( diff --git a/packages/frontend/core/src/components/affine/auth/send-email.tsx b/packages/frontend/core/src/components/affine/auth/send-email.tsx deleted file mode 100644 index f2675ee72ddaa..0000000000000 --- a/packages/frontend/core/src/components/affine/auth/send-email.tsx +++ /dev/null @@ -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; - 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 ( - <> - - {content} - - - - - - - - - ); -}; diff --git a/packages/frontend/core/src/components/affine/auth/sign-in.tsx b/packages/frontend/core/src/components/affine/auth/sign-in.tsx deleted file mode 100644 index 6cc58ba1aaad2..0000000000000 --- a/packages/frontend/core/src/components/affine/auth/sign-in.tsx +++ /dev/null @@ -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> = ({ - 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 ( - <> - - - - -
- - - {verifyToken || !needCaptcha ? ( - - ) : ( - - )} - - {errorMsg &&
{errorMsg}
} - -
- {/*prettier-ignore*/} - - By clicking "Continue with Google/Email" above, you acknowledge that - you agree to AFFiNE's Terms of Conditions and Privacy Policy. - -
-
- - {onSkip ? ( - <> -
-
- or -
-
-
-
- {t['com.affine.mobile.sign-in.skip.hint']()} -
- -
- - ) : null} - - ); -}; diff --git a/packages/frontend/core/src/components/affine/auth/style.css.ts b/packages/frontend/core/src/components/affine/auth/style.css.ts index 64257cb7319a7..5fcab0b3f11ee 100644 --- a/packages/frontend/core/src/components/affine/auth/style.css.ts +++ b/packages/frontend/core/src/components/affine/auth/style.css.ts @@ -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, -}); - -export const errorMessage = style({ - marginTop: '30px', - color: cssVar('textHighlightForegroundRed'), - fontSize: cssVar('fontXs'), - lineHeight: 1.5, -}); +import { style } from '@vanilla-extract/css'; -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', -}); diff --git a/packages/frontend/core/src/components/affine/auth/subscription-redirect.css.ts b/packages/frontend/core/src/components/affine/auth/subscription-redirect.css.ts deleted file mode 100644 index 1514a6a224d70..0000000000000 --- a/packages/frontend/core/src/components/affine/auth/subscription-redirect.css.ts +++ /dev/null @@ -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', -}); diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx index ceb219996a7bf..71e14603b3e7c 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx @@ -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 ; } return ; diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx index dabe378626937..6ba1d42bec2ff 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx @@ -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 ; - } else if ( - props.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD - ) { + } else { return ( // TODO(@eyhn): refactor this part @@ -330,5 +327,4 @@ export const SharePage = (props: ShareMenuProps) => { ); } - throw new Error('Unreachable'); }; diff --git a/packages/frontend/core/src/components/atoms/index.ts b/packages/frontend/core/src/components/atoms/index.ts index 00011f2b44bde..8cd17bc60caf7 100644 --- a/packages/frontend/core/src/components/atoms/index.ts +++ b/packages/frontend/core/src/components/atoms/index.ts @@ -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('docs'); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/setup-provider.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/setup-provider.tsx index 89c544e5c9fb1..ebd4615aa0f54 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/setup-provider.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/setup-provider.tsx @@ -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>(); -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; @@ -496,10 +498,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(); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 61f251d2d775f..08499929993a4 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -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' ? ( } data-testid="editor-option-menu-link" @@ -426,7 +424,7 @@ export const PageHeaderMenuButton = ({ > - {workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? ( + {workspace.flavour !== 'local' ? ( { - 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 ( +
+ + ), + }} + /> +
+ + + ); +}; diff --git a/packages/frontend/core/src/components/affine/auth/use-captcha.tsx b/packages/frontend/core/src/components/sign-in/captcha.tsx similarity index 100% rename from packages/frontend/core/src/components/affine/auth/use-captcha.tsx rename to packages/frontend/core/src/components/sign-in/captcha.tsx diff --git a/packages/frontend/core/src/components/sign-in/index.tsx b/packages/frontend/core/src/components/sign-in/index.tsx new file mode 100644 index 0000000000000..8b164d6cd84c1 --- /dev/null +++ b/packages/frontend/core/src/components/sign-in/index.tsx @@ -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({ + step: initialServerBaseUrl ? 'addSelfhosted' : 'signIn', + initialServerBaseUrl: initialServerBaseUrl, + }); + + const defaultServerService = useService(DefaultServerService); + + const step = state.step; + const server = state.server ?? defaultServerService.server; + + return ( + + {step === 'signIn' ? ( + + ) : step === 'signInWithEmail' ? ( + + ) : step === 'signInWithPassword' ? ( + + ) : step === 'addSelfhosted' ? ( + + ) : null} + + ); +}; diff --git a/packages/frontend/core/src/components/affine/auth/after-sign-in-send-email.tsx b/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx similarity index 64% rename from packages/frontend/core/src/components/affine/auth/after-sign-in-send-email.tsx rename to packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx index 91d931660a16f..4fab30e7d1059 100644 --- a/packages/frontend/core/src/components/affine/auth/after-sign-in-send-email.tsx +++ b/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx @@ -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>; + 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 ? ( + <> + + + + + + + ) : ( <> {resendCountDown <= 0 ? ( - <> - - - + ) : (
diff --git a/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx b/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx similarity index 67% rename from packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx rename to packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx index 1c4508c51c06f..e0d7b72b25604 100644 --- a/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx +++ b/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx @@ -6,27 +6,49 @@ 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 { 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> = ({ - setAuthData, - email, - redirectUrl, +export const SignInWithPasswordStep = ({ + state, + changeState, + close, +}: { + state: SignInState; + changeState: Dispatch>; + 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 serverName = useLiveData( + serverService.server.config$.selector(c => c.serverName) + ); + const isEmailEnabled = useLiveData( + serverService.server.config$.selector(c => c.credentialsRequirement.email) + ); const verifyToken = useLiveData(captchaService.verifyToken$); const needCaptcha = useLiveData(captchaService.needCaptcha$); @@ -34,8 +56,20 @@ export const SignInWithPassword: FC> = ({ 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 +89,7 @@ export const SignInWithPassword: FC> = ({ }, [ isLoading, verifyToken, + needCaptcha, captchaService, authService, email, @@ -66,14 +101,7 @@ export const SignInWithPassword: FC> = ({ 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 +110,13 @@ export const SignInWithPassword: FC> = ({ // 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 ( <> > = ({ errorHint={t['com.affine.auth.password.error']()} onEnter={onSignIn} /> - - {(verifyToken || !needCaptcha) && ( -
+ {isEmailEnabled && ( +
> = ({ { - setAuthData({ state: 'signIn' }); - }, [setAuthData])} + changeState(prev => ({ ...prev, step: 'signIn' })); + }, [changeState])} /> ); diff --git a/packages/frontend/core/src/components/sign-in/sign-in.tsx b/packages/frontend/core/src/components/sign-in/sign-in.tsx new file mode 100644 index 0000000000000..d386981876079 --- /dev/null +++ b/packages/frontend/core/src/components/sign-in/sign-in.tsx @@ -0,0 +1,198 @@ +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>; + close: () => void; +}) => { + const t = useI18n(); + const serverService = useService(ServerService); + const serverName = useLiveData( + serverService.server.config$.selector(c => c.serverName) + ); + const isSelfhost = useLiveData( + serverService.server.config$.selector( + c => c.type === ServerDeploymentType.Selfhosted + ) + ); + const isEmailEnabled = useLiveData( + serverService.server.config$.selector(c => c.credentialsRequirement.email) + ); + 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 } = + 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 || !isEmailEnabled) { + changeState(prev => ({ + ...prev, + email, + step: 'signInWithPassword', + })); + } else { + changeState(prev => ({ + ...prev, + email, + step: 'signInWithEmail', + })); + } + } else { + changeState(prev => ({ + ...prev, + email, + step: 'signInWithEmail', + })); + } + } 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, isEmailEnabled]); + + const onAddSelfhosted = useCallback(() => { + changeState(prev => ({ + ...prev, + step: 'addSelfhosted', + })); + }, [changeState]); + + return ( + <> + + + + + + + ); +}; diff --git a/packages/frontend/core/src/components/sign-in/style.css.ts b/packages/frontend/core/src/components/sign-in/style.css.ts new file mode 100644 index 0000000000000..80316b9354097 --- /dev/null +++ b/packages/frontend/core/src/components/sign-in/style.css.ts @@ -0,0 +1,97 @@ +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, + paddingLeft: 0, + paddingRight: 5, + marginBottom: 24, + color: cssVarV2('text/link'), +}); + +export const addSelfhostedButtonPrefix = style({ + color: cssVarV2('text/link'), +}); + +export const orDivider = style({ + marginTop: 24, + textAlign: 'center', + display: 'flex', + alignItems: 'center', + gap: 8, + selectors: { + '&::before, &::after': { + content: '', + flex: 1, + borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`, + }, + }, +}); diff --git a/packages/frontend/core/src/components/top-tip.tsx b/packages/frontend/core/src/components/top-tip.tsx index 66c4540be3ac8..0e703a3d1b6f4 100644 --- a/packages/frontend/core/src/components/top-tip.tsx +++ b/packages/frontend/core/src/components/top-tip.tsx @@ -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 ( { - 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 ( { - setOpenSignIn(state => ({ - ...state, - openModal: true, - })); - }, [setOpenSignIn]); + globalDialogService.open('sign-in', {}); + }, [globalDialogService]); const onNewWorkspace = useCallback(() => { if ( diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts index 31487708ba25e..455943f6bfdf5 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts @@ -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', diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx index ae91d500ecddc..0f38278ec68b3 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx @@ -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) => { - 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 (
-
- - {t['com.affine.workspaceList.workspaceListType.cloud']()} +
+
+ + {serverName} -  + {account ? account.email : 'Not signed in'} +
+ + Delete Server + + ), + accountStatus === 'authenticated' && ( + + Sign Out + + ), + accountStatus === 'unauthenticated' && ( + + Sign In + + ), + ]} + > + } /> +
); @@ -68,13 +150,15 @@ const LocalWorkspaces = ({ } return (
-
- - {t['com.affine.workspaceList.workspaceListType.local']()} +
+
+ + {t['com.affine.workspaceList.workspaceListType.local']()} +
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 ? ( -
- - {localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? ( +
+ {servers.map(server => ( + + flavour === server.id + )} + onClickWorkspace={handleClickWorkspace} + onClickWorkspaceSetting={ + showSettingsButton ? onClickWorkspaceSetting : undefined + } + /> - ) : null} -
- ) : null} + + ))} +
{ 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 ? ( ) : ( @@ -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< )}
- {onClickEnableCloud && - workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? ( + {onClickEnableCloud && workspaceMetadata.flavour === 'local' ? ( + + ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx index efc8efc9d13b9..1ae9c5d025237 100644 --- a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx @@ -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); diff --git a/packages/frontend/core/src/desktop/dialogs/import-template/index.tsx b/packages/frontend/core/src/desktop/dialogs/import-template/index.tsx index e4e9e263e1085..79fe01d3133a5 100644 --- a/packages/frontend/core/src/desktop/dialogs/import-template/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/import-template/index.tsx @@ -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(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 ); diff --git a/packages/frontend/core/src/desktop/dialogs/import-workspace/index.tsx b/packages/frontend/core/src/desktop/dialogs/import-workspace/index.tsx index 0c4af461c0559..dafb9b90617f0 100644 --- a/packages/frontend/core/src/desktop/dialogs/import-workspace/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/import-workspace/index.tsx @@ -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, }, }); diff --git a/packages/frontend/core/src/desktop/dialogs/index.tsx b/packages/frontend/core/src/desktop/dialogs/index.tsx index b97ce58e2ce34..641a4ae448380 100644 --- a/packages/frontend/core/src/desktop/dialogs/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/index.tsx @@ -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 @@ -66,8 +71,6 @@ export const GlobalDialogs = () => { /> ); })} - - ); }; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/account-setting/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/account-setting/index.tsx index d69fff7db1ad0..3a89875c706dc 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/account-setting/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/account-setting/index.tsx @@ -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 ( <> diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/login.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/login.tsx index 68bad88989c31..2feed31242e29 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/login.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/login.tsx @@ -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 ( + + ); +}; diff --git a/packages/frontend/core/src/desktop/pages/auth/sign-in.tsx b/packages/frontend/core/src/desktop/pages/auth/sign-in.tsx index ecebc15cd3167..f58c25e2f6e92 100644 --- a/packages/frontend/core/src/desktop/pages/auth/sign-in.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/sign-in.tsx @@ -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 (
- +
); diff --git a/packages/frontend/core/src/desktop/pages/index/index.tsx b/packages/frontend/core/src/desktop/pages/index/index.tsx index e466ec17f9622..38fa4b66edc76 100644 --- a/packages/frontend/core/src/desktop/pages/index/index.tsx +++ b/packages/frontend/core/src/desktop/pages/index/index.tsx @@ -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; diff --git a/packages/frontend/core/src/desktop/pages/workspace/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/index.tsx index a973556d3514a..f974e58a71fef 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/index.tsx @@ -1,5 +1,6 @@ import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout'; import { workbenchRoutes } from '@affine/core/desktop/workbench-router'; +import { WorkspaceServerService } from '@affine/core/modules/cloud'; import { ZipTransformer } from '@blocksuite/affine/blocks'; import type { Workspace, WorkspaceMetadata } from '@toeverything/infra'; import { @@ -189,9 +190,13 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => { }; localStorage.setItem('last_workspace_id', workspace.id); globalContextService.globalContext.workspaceId.set(workspace.id); + globalContextService.globalContext.workspaceFlavour.set( + workspace.flavour + ); return () => { window.currentWorkspace = undefined; globalContextService.globalContext.workspaceId.set(null); + globalContextService.globalContext.workspaceFlavour.set(null); }; } return; @@ -201,21 +206,27 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => { return null; // skip this, workspace will be set in layout effect } + const workspaceServer = workspace.scope.get(WorkspaceServerService).server; + if (!isRootDocReady) { return ( - - + + + + ); } return ( - - - - - - + + + + + + + + ); }; diff --git a/packages/frontend/core/src/desktop/pages/workspace/layouts/workspace-layout.tsx b/packages/frontend/core/src/desktop/pages/workspace/layouts/workspace-layout.tsx index 2807198ce1f28..125080f0c59c6 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/layouts/workspace-layout.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/layouts/workspace-layout.tsx @@ -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({ {/* ---- some side-effect components ---- */} - {currentWorkspace?.flavour === WorkspaceFlavour.LOCAL && ( + {currentWorkspace?.flavour === 'local' ? ( - )} - {currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && ( + ) : ( )} diff --git a/packages/frontend/core/src/desktop/pages/workspace/share/share-header.tsx b/packages/frontend/core/src/desktop/pages/workspace/share/share-header.tsx index b8a98ee44b0da..0e23f1d029839 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/share/share-header.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/share/share-header.tsx @@ -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} /> -
); } diff --git a/packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx index 69849f9c8bfc7..582bf52abef28 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx @@ -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, }, diff --git a/packages/frontend/core/src/mobile/views/sign-in/art-dark.inline.svg b/packages/frontend/core/src/mobile/components/sign-in/art-dark.inline.svg similarity index 100% rename from packages/frontend/core/src/mobile/views/sign-in/art-dark.inline.svg rename to packages/frontend/core/src/mobile/components/sign-in/art-dark.inline.svg diff --git a/packages/frontend/core/src/mobile/views/sign-in/art-light.inline.svg b/packages/frontend/core/src/mobile/components/sign-in/art-light.inline.svg similarity index 100% rename from packages/frontend/core/src/mobile/views/sign-in/art-light.inline.svg rename to packages/frontend/core/src/mobile/components/sign-in/art-light.inline.svg diff --git a/packages/frontend/core/src/mobile/views/sign-in/background.css.ts b/packages/frontend/core/src/mobile/components/sign-in/background.css.ts similarity index 100% rename from packages/frontend/core/src/mobile/views/sign-in/background.css.ts rename to packages/frontend/core/src/mobile/components/sign-in/background.css.ts diff --git a/packages/frontend/core/src/mobile/views/sign-in/background.tsx b/packages/frontend/core/src/mobile/components/sign-in/background.tsx similarity index 100% rename from packages/frontend/core/src/mobile/views/sign-in/background.tsx rename to packages/frontend/core/src/mobile/components/sign-in/background.tsx diff --git a/packages/frontend/core/src/mobile/components/sign-in/index.tsx b/packages/frontend/core/src/mobile/components/sign-in/index.tsx new file mode 100644 index 0000000000000..5bfc891258242 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/sign-in/index.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/frontend/core/src/mobile/views/sign-in/layout.css.ts b/packages/frontend/core/src/mobile/components/sign-in/layout.css.ts similarity index 100% rename from packages/frontend/core/src/mobile/views/sign-in/layout.css.ts rename to packages/frontend/core/src/mobile/components/sign-in/layout.css.ts diff --git a/packages/frontend/core/src/mobile/views/sign-in/layout.tsx b/packages/frontend/core/src/mobile/components/sign-in/layout.tsx similarity index 100% rename from packages/frontend/core/src/mobile/views/sign-in/layout.tsx rename to packages/frontend/core/src/mobile/components/sign-in/layout.tsx diff --git a/packages/frontend/core/src/mobile/components/workspace-selector/menu.tsx b/packages/frontend/core/src/mobile/components/workspace-selector/menu.tsx index fd57b7e59aaba..42d757348d89a 100644 --- a/packages/frontend/core/src/mobile/components/workspace-selector/menu.tsx +++ b/packages/frontend/core/src/mobile/components/workspace-selector/menu.tsx @@ -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] ); diff --git a/packages/frontend/core/src/mobile/dialogs/index.tsx b/packages/frontend/core/src/mobile/dialogs/index.tsx index e2fc222b182bb..0440d8e468ff1 100644 --- a/packages/frontend/core/src/mobile/dialogs/index.tsx +++ b/packages/frontend/core/src/mobile/dialogs/index.tsx @@ -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 @@ -58,8 +59,6 @@ export const GlobalDialogs = () => { /> ); })} - - ); }; diff --git a/packages/frontend/core/src/mobile/dialogs/setting/user-profile/index.tsx b/packages/frontend/core/src/mobile/dialogs/setting/user-profile/index.tsx index 849374bc1c353..3a6337c3e5177 100644 --- a/packages/frontend/core/src/mobile/dialogs/setting/user-profile/index.tsx +++ b/packages/frontend/core/src/mobile/dialogs/setting/user-profile/index.tsx @@ -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 ( setAuthModal({ openModal: true, state: 'signIn' })} + onClick={() => globalDialogService.open('sign-in', {})} avatar={} title="Sign up / Sign in" caption="Sync with AFFiNE Cloud" diff --git a/packages/frontend/core/src/mobile/views/sign-in/modal.tsx b/packages/frontend/core/src/mobile/dialogs/sign-in/index.tsx similarity index 54% rename from packages/frontend/core/src/mobile/views/sign-in/modal.tsx rename to packages/frontend/core/src/mobile/dialogs/sign-in/index.tsx index c9c4f4c098bab..78be0e2918f54 100644 --- a/packages/frontend/core/src/mobile/views/sign-in/modal.tsx +++ b/packages/frontend/core/src/mobile/dialogs/sign-in/index.tsx @@ -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) => { return ( close()} contentOptions={{ style: { padding: 0, @@ -35,7 +27,7 @@ export const MobileSignInModal = () => { }} withoutCloseButton > - + { variant="solid" icon={} style={{ borderRadius: 8, padding: 4 }} - onClick={closeModal} + onClick={() => close()} /> diff --git a/packages/frontend/core/src/mobile/pages/root/index.tsx b/packages/frontend/core/src/mobile/pages/root/index.tsx index 93e473b4b34dd..0dccf2bf6bd9d 100644 --- a/packages/frontend/core/src/mobile/pages/root/index.tsx +++ b/packages/frontend/core/src/mobile/pages/root/index.tsx @@ -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 = () => { - ); diff --git a/packages/frontend/core/src/mobile/pages/sign-in.tsx b/packages/frontend/core/src/mobile/pages/sign-in.tsx index ae616123eeb6c..9df2c216d2d2b 100644 --- a/packages/frontend/core/src/mobile/pages/sign-in.tsx +++ b/packages/frontend/core/src/mobile/pages/sign-in.tsx @@ -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 navigate('/')} />; + return navigate('/')} />; }; diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-share-button.tsx b/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-share-button.tsx index 94c8077a3360a..c1d83a980f052 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-share-button.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-share-button.tsx @@ -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; } diff --git a/packages/frontend/core/src/mobile/pages/workspace/layout.tsx b/packages/frontend/core/src/mobile/pages/workspace/layout.tsx index 850dccfac1d78..5d24ff77ec525 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/layout.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/layout.tsx @@ -6,8 +6,8 @@ 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 { 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, @@ -75,9 +75,13 @@ export const WorkspaceLayout = ({ ); localStorage.setItem('last_workspace_id', workspace.id); globalContextService.globalContext.workspaceId.set(workspace.id); + globalContextService.globalContext.workspaceFlavour.set( + workspace.flavour + ); return () => { window.currentWorkspace = undefined; globalContextService.globalContext.workspaceId.set(null); + globalContextService.globalContext.workspaceFlavour.set(null); }; } return; @@ -94,23 +98,28 @@ export const WorkspaceLayout = ({ return ; } + const workspaceServer = workspace.scope.get(WorkspaceServerService).server; + return ( - - - - + + + + + - {/* ---- some side-effect components ---- */} - - {workspace?.flavour === WorkspaceFlavour.LOCAL && } - {workspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && ( - - )} - - - {children} - - + {/* ---- some side-effect components ---- */} + + {workspace?.flavour === 'local' ? ( + + ) : ( + + )} + + + {children} + + + ); }; diff --git a/packages/frontend/core/src/mobile/views/sign-in/mobile-sign-in.tsx b/packages/frontend/core/src/mobile/views/sign-in/mobile-sign-in.tsx deleted file mode 100644 index 7005236b3cba2..0000000000000 --- a/packages/frontend/core/src/mobile/views/sign-in/mobile-sign-in.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { AuthPanel } from '@affine/core/components/affine/auth'; - -import { MobileSignInLayout } from './layout'; - -export const MobileSignIn = ({ onSkip }: { onSkip: () => void }) => { - return ( - - - - ); -}; diff --git a/packages/frontend/core/src/modules/cloud/constant.ts b/packages/frontend/core/src/modules/cloud/constant.ts index fa05e088f0587..52cb74759eeb7 100644 --- a/packages/frontend/core/src/modules/cloud/constant.ts +++ b/packages/frontend/core/src/modules/cloud/constant.ts @@ -25,6 +25,7 @@ export const BUILD_IN_SERVERS: (ServerMetadata & { config: ServerConfig })[] = minLength: 8, maxLength: 32, }, + email: false, }, }, }, @@ -49,6 +50,7 @@ export const BUILD_IN_SERVERS: (ServerMetadata & { config: ServerConfig })[] = minLength: 8, maxLength: 32, }, + email: true, }, }, }, @@ -73,6 +75,7 @@ export const BUILD_IN_SERVERS: (ServerMetadata & { config: ServerConfig })[] = minLength: 8, maxLength: 32, }, + email: true, }, }, }, @@ -97,6 +100,7 @@ export const BUILD_IN_SERVERS: (ServerMetadata & { config: ServerConfig })[] = minLength: 8, maxLength: 32, }, + email: true, }, }, }, @@ -121,6 +125,7 @@ export const BUILD_IN_SERVERS: (ServerMetadata & { config: ServerConfig })[] = minLength: 8, maxLength: 32, }, + email: true, }, }, }, @@ -145,6 +150,7 @@ export const BUILD_IN_SERVERS: (ServerMetadata & { config: ServerConfig })[] = minLength: 8, maxLength: 32, }, + email: true, }, }, }, diff --git a/packages/frontend/core/src/modules/cloud/entities/server.ts b/packages/frontend/core/src/modules/cloud/entities/server.ts index 8960745ec99e5..28d218ce91b34 100644 --- a/packages/frontend/core/src/modules/cloud/entities/server.ts +++ b/packages/frontend/core/src/modules/cloud/entities/server.ts @@ -68,7 +68,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, diff --git a/packages/frontend/core/src/modules/cloud/entities/session.ts b/packages/frontend/core/src/modules/cloud/entities/session.ts index 52c0ccf580ee5..f6f63b5975a07 100644 --- a/packages/frontend/core/src/modules/cloud/entities/session.ts +++ b/packages/frontend/core/src/modules/cloud/entities/session.ts @@ -68,7 +68,7 @@ export class AuthSession extends Entity { revalidate = effect( exhaustMapWithTrailing(() => - fromPromise(this.getSession()).pipe( + fromPromise(() => this.getSession()).pipe( backoffRetry({ count: Infinity, }), diff --git a/packages/frontend/core/src/modules/cloud/events/account-changed.ts b/packages/frontend/core/src/modules/cloud/events/account-changed.ts new file mode 100644 index 0000000000000..76bfa8f954b8a --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/events/account-changed.ts @@ -0,0 +1,7 @@ +import { createEvent } from '@toeverything/infra'; + +import type { AuthAccountInfo } from '../entities/session'; + +export const AccountChanged = createEvent( + 'AccountChanged' +); diff --git a/packages/frontend/core/src/modules/cloud/events/account-logged-in.ts b/packages/frontend/core/src/modules/cloud/events/account-logged-in.ts new file mode 100644 index 0000000000000..c97751ce428ab --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/events/account-logged-in.ts @@ -0,0 +1,5 @@ +import { createEvent } from '@toeverything/infra'; + +import type { AuthAccountInfo } from '../entities/session'; + +export const AccountLoggedIn = createEvent('AccountLoggedIn'); diff --git a/packages/frontend/core/src/modules/cloud/events/account-logged-out.ts b/packages/frontend/core/src/modules/cloud/events/account-logged-out.ts new file mode 100644 index 0000000000000..8e914f2848488 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/events/account-logged-out.ts @@ -0,0 +1,6 @@ +import { createEvent } from '@toeverything/infra'; + +import type { AuthAccountInfo } from '../entities/session'; + +export const AccountLoggedOut = + createEvent('AccountLoggedOut'); diff --git a/packages/frontend/core/src/modules/cloud/events/server-initialized.ts b/packages/frontend/core/src/modules/cloud/events/server-initialized.ts new file mode 100644 index 0000000000000..804701ef7f3ac --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/events/server-initialized.ts @@ -0,0 +1,5 @@ +import { createEvent } from '@toeverything/infra'; + +import type { Server } from '../entities/server'; + +export const ServerInitialized = createEvent('ServerInitialized'); diff --git a/packages/frontend/core/src/modules/cloud/events/server-started.ts b/packages/frontend/core/src/modules/cloud/events/server-started.ts new file mode 100644 index 0000000000000..c8c929d8776b7 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/events/server-started.ts @@ -0,0 +1,3 @@ +import { createEvent } from '@toeverything/infra'; + +export const ServerStarted = createEvent('ServerStarted'); diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts index 6d8c27cf57e04..eaa08eacea579 100644 --- a/packages/frontend/core/src/modules/cloud/index.ts +++ b/packages/frontend/core/src/modules/cloud/index.ts @@ -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, [ diff --git a/packages/frontend/core/src/modules/cloud/services/auth.ts b/packages/frontend/core/src/modules/cloud/services/auth.ts index 5dfc6a00685c2..c27f19d2bfa53 100644 --- a/packages/frontend/core/src/modules/cloud/services/auth.ts +++ b/packages/frontend/core/src/modules/cloud/services/auth.ts @@ -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( - 'AccountChanged' -); - -export const AccountLoggedIn = createEvent('AccountLoggedIn'); - -export const AccountLoggedOut = - createEvent('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); @@ -70,11 +58,12 @@ export class AuthService extends Service { } else { this.eventBus.emit(AccountLoggedIn, account); } + console.log('account changed'); this.eventBus.emit(AccountChanged, account); }); } - private onApplicationStart() { + private onServerStarted() { this.session.revalidate(); } diff --git a/packages/frontend/core/src/modules/cloud/services/captcha.ts b/packages/frontend/core/src/modules/cloud/services/captcha.ts index 5c41509cbbdce..f891891abdd4f 100644 --- a/packages/frontend/core/src/modules/cloud/services/captcha.ts +++ b/packages/frontend/core/src/modules/cloud/services/captcha.ts @@ -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; + }) + ); + }) + ); } diff --git a/packages/frontend/core/src/modules/cloud/services/servers.ts b/packages/frontend/core/src/modules/cloud/services/servers.ts index 4c4cb322238cd..e0e3084947a78 100644 --- a/packages/frontend/core/src/modules/cloud/services/servers.ts +++ b/packages/frontend/core/src/modules/cloud/services/servers.ts @@ -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(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; + } + } } diff --git a/packages/frontend/core/src/modules/cloud/services/subscription.ts b/packages/frontend/core/src/modules/cloud/services/subscription.ts index 9d3fb60117ca6..eb79d3da4316e 100644 --- a/packages/frontend/core/src/modules/cloud/services/subscription.ts +++ b/packages/frontend/core/src/modules/cloud/services/subscription.ts @@ -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 { diff --git a/packages/frontend/core/src/modules/cloud/services/user-copilot-quota.ts b/packages/frontend/core/src/modules/cloud/services/user-copilot-quota.ts index 740c3804693bf..eeb5e039028f1 100644 --- a/packages/frontend/core/src/modules/cloud/services/user-copilot-quota.ts +++ b/packages/frontend/core/src/modules/cloud/services/user-copilot-quota.ts @@ -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 { diff --git a/packages/frontend/core/src/modules/cloud/services/user-feature.ts b/packages/frontend/core/src/modules/cloud/services/user-feature.ts index 5abbdbbde908d..2e554c986234e 100644 --- a/packages/frontend/core/src/modules/cloud/services/user-feature.ts +++ b/packages/frontend/core/src/modules/cloud/services/user-feature.ts @@ -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 { diff --git a/packages/frontend/core/src/modules/cloud/services/user-quota.ts b/packages/frontend/core/src/modules/cloud/services/user-quota.ts index bb8b931e094fd..1c6bca80adf74 100644 --- a/packages/frontend/core/src/modules/cloud/services/user-quota.ts +++ b/packages/frontend/core/src/modules/cloud/services/user-quota.ts @@ -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 { diff --git a/packages/frontend/core/src/modules/cloud/services/websocket.ts b/packages/frontend/core/src/modules/cloud/services/websocket.ts index 29def8313022a..6d6fefe9214cc 100644 --- a/packages/frontend/core/src/modules/cloud/services/websocket.ts +++ b/packages/frontend/core/src/modules/cloud/services/websocket.ts @@ -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) diff --git a/packages/frontend/core/src/modules/cloud/stores/auth.ts b/packages/frontend/core/src/modules/cloud/stores/auth.ts index abc9c73d0cd59..c40c24888a0b6 100644 --- a/packages/frontend/core/src/modules/cloud/stores/auth.ts +++ b/packages/frontend/core/src/modules/cloud/stores/auth.ts @@ -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('affine-cloud-auth'); + return this.globalState.watch( + `${this.serverService.server.id}-auth` + ); } getCachedAuthSession() { - return this.globalState.get('affine-cloud-auth'); + return this.globalState.get( + `${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() { diff --git a/packages/frontend/core/src/modules/cloud/stores/server-config.ts b/packages/frontend/core/src/modules/cloud/stores/server-config.ts index a8a34ffddeaca..21469d509f807 100644 --- a/packages/frontend/core/src/modules/cloud/stores/server-config.ts +++ b/packages/frontend/core/src/modules/cloud/stores/server-config.ts @@ -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 { - 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, diff --git a/packages/frontend/core/src/modules/cloud/stores/server-list.ts b/packages/frontend/core/src/modules/cloud/stores/server-list.ts index c8cedfe3bf2bc..8b7cf1d13c0d3 100644 --- a/packages/frontend/core/src/modules/cloud/stores/server-list.ts +++ b/packages/frontend/core/src/modules/cloud/stores/server-list.ts @@ -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('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('serverList', [ ...oldServers, server, diff --git a/packages/frontend/core/src/modules/desktop-api/service/desktop-api.ts b/packages/frontend/core/src/modules/desktop-api/service/desktop-api.ts index bf59cc5fb398f..e44a6e1ad59e6 100644 --- a/packages/frontend/core/src/modules/desktop-api/service/desktop-api.ts +++ b/packages/frontend/core/src/modules/desktop-api/service/desktop-api.ts @@ -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': { diff --git a/packages/frontend/core/src/modules/dialogs/constant.ts b/packages/frontend/core/src/modules/dialogs/constant.ts index 04fd38a5861c8..86e8ea8e7a12b 100644 --- a/packages/frontend/core/src/modules/dialogs/constant.ts +++ b/packages/frontend/core/src/modules/dialogs/constant.ts @@ -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 = { diff --git a/packages/frontend/core/src/modules/favorite/stores/favorite.ts b/packages/frontend/core/src/modules/favorite/stores/favorite.ts index 5f0a62745e146..2c1cb1bc96415 100644 --- a/packages/frontend/core/src/modules/favorite/stores/favorite.ts +++ b/packages/frontend/core/src/modules/favorite/stores/favorite.ts @@ -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__')); diff --git a/packages/frontend/core/src/modules/import-template/services/import.ts b/packages/frontend/core/src/modules/import-template/services/import.ts index af8183d6ab5b9..27ae74f783346 100644 --- a/packages/frontend/core/src/modules/import-template/services/import.ts +++ b/packages/frontend/core/src/modules/import-template/services/import.ts @@ -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 diff --git a/packages/frontend/core/src/modules/permissions/entities/permission.ts b/packages/frontend/core/src/modules/permissions/entities/permission.ts index ca7afa8163e0a..19c8bc8c662d4 100644 --- a/packages/frontend/core/src/modules/permissions/entities/permission.ts +++ b/packages/frontend/core/src/modules/permissions/entities/permission.ts @@ -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 diff --git a/packages/frontend/core/src/modules/share-doc/services/share-docs-list.ts b/packages/frontend/core/src/modules/share-doc/services/share-docs-list.ts index 90df05b654c70..714b5e0189c61 100644 --- a/packages/frontend/core/src/modules/share-doc/services/share-docs-list.ts +++ b/packages/frontend/core/src/modules/share-doc/services/share-docs-list.ts @@ -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; } diff --git a/packages/frontend/core/src/modules/telemetry/index.ts b/packages/frontend/core/src/modules/telemetry/index.ts index 5a7d827ca0067..feaa282bdae0d 100644 --- a/packages/frontend/core/src/modules/telemetry/index.ts +++ b/packages/frontend/core/src/modules/telemetry/index.ts @@ -1,8 +1,11 @@ import { type Framework, GlobalContextService } from '@toeverything/infra'; -import { ServersService } from '../cloud/services/servers'; +import { DefaultServerService } from '../cloud'; import { TelemetryService } from './services/telemetry'; export function configureTelemetryModule(framework: Framework) { - framework.service(TelemetryService, [ServersService, GlobalContextService]); + framework.service(TelemetryService, [ + GlobalContextService, + DefaultServerService, + ]); } diff --git a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts index 2646a8292e291..7fc0be118aa17 100644 --- a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts +++ b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts @@ -2,27 +2,39 @@ import { mixpanel } from '@affine/track'; import type { GlobalContextService } from '@toeverything/infra'; import { ApplicationStarted, 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 { + AccountChanged, + type AuthAccountInfo, + AuthService, + type DefaultServerService, +} 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)[] = []; + constructor( - serversService: ServersService, - private readonly globalContextService: GlobalContextService + private readonly globalContextService: GlobalContextService, + defaultServerService: DefaultServerService ) { 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); + this.authService = defaultServerService.server.scope.get(AuthService); + + this.disposableFns.push( + defaultServerService.server.eventBus.on( + AccountChanged, + account => this.updateIdentity(account) + ) + ); + this.disposableFns.push( + defaultServerService.server.eventBus.on(AccountChanged, () => + this.onAccountLoggedOut() + ) + ); } onApplicationStart() { @@ -80,7 +92,7 @@ export class TelemetryService extends Service { } override dispose(): void { - this.disposables.forEach(dispose => dispose()); + this.disposableFns.forEach(dispose => dispose()); super.dispose(); } } diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts index 8fe0b8a3e7506..dbcbd4ad6663d 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts @@ -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,42 @@ 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, + () => { + console.log('account changed'); + this.revalidate(); + } + ); } - flavour: WorkspaceFlavour = WorkspaceFlavour.AFFINE_CLOUD; + flavour = this.server.id; async deleteWorkspace(id: string): Promise { await this.graphqlService.gql({ @@ -95,6 +95,7 @@ export class CloudWorkspaceFlavourProviderService this.revalidate(); await this.waitForLoaded(); } + async createWorkspace( initial: ( docCollection: DocCollection, @@ -139,11 +140,12 @@ export class CloudWorkspaceFlavourProviderService return { id: workspaceId, - flavour: WorkspaceFlavour.AFFINE_CLOUD, + flavour: this.server.id, }; } revalidate = effect( map(() => { + console.log('revalidate', this.authService.session.account$.value); return { accountId: this.authService.session.account$.value?.id }; }), exhaustMapSwitchUntilChanged( @@ -169,7 +171,7 @@ export class CloudWorkspaceFlavourProviderService accountId, workspaces: ids.map(({ id, initialized }) => ({ id, - flavour: WorkspaceFlavour.AFFINE_CLOUD, + flavour: this.server.id, initialized, })), }; @@ -181,7 +183,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 +204,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 +299,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 +317,62 @@ export class CloudWorkspaceFlavourProviderService private waitForLoaded() { return this.isRevalidating$.waitFor(loading => !loading); } + + dispose() { + console.log('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( + 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(subscribe => { + subscribe.next(refs.map(ref => ref.obj)); + return () => { + refs.forEach(ref => { + ref.release(); + }); + }; + }); + }) + ), + [] as any + ); + + private readonly pool = new ObjectPool( + { + onDelete(obj) { + obj.dispose(); + }, + } + ); } diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts index e2a786c1442fe..79fe8c71946dd 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts @@ -1,11 +1,12 @@ import { DebugLogger } from '@affine/debug'; -import { WorkspaceFlavour } from '@affine/env/workspace'; import { DocCollection } from '@blocksuite/affine/store'; import type { BlobStorage, DocStorage, + FrameworkProvider, WorkspaceEngineProvider, WorkspaceFlavourProvider, + WorkspaceFlavoursProvider, WorkspaceMetadata, WorkspaceProfileInfo, } from '@toeverything/infra'; @@ -54,17 +55,13 @@ export function setLocalWorkspaceIds( ); } -export class LocalWorkspaceFlavourProvider - extends Service - implements WorkspaceFlavourProvider -{ +class LocalWorkspaceFlavourProvider implements WorkspaceFlavourProvider { constructor( - private readonly storageProvider: WorkspaceEngineStorageProvider - ) { - super(); - } + private readonly storageProvider: WorkspaceEngineStorageProvider, + private readonly framework: FrameworkProvider + ) {} - flavour: WorkspaceFlavour = WorkspaceFlavour.LOCAL; + flavour = 'local'; notifyChannel = new BroadcastChannel( LOCAL_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY ); @@ -72,9 +69,8 @@ export class LocalWorkspaceFlavourProvider async deleteWorkspace(id: string): Promise { setLocalWorkspaceIds(ids => ids.filter(x => x !== id)); - const electronApi = this.framework.getOptional(DesktopApiService); - - if (BUILD_CONFIG.isElectron && electronApi) { + if (BUILD_CONFIG.isElectron) { + const electronApi = this.framework.get(DesktopApiService); await electronApi.handler.workspace.delete(id); } @@ -116,7 +112,7 @@ export class LocalWorkspaceFlavourProvider // notify all browser tabs, so they can update their workspace list this.notifyChannel.postMessage(id); - return { id, flavour: WorkspaceFlavour.LOCAL }; + return { id, flavour: 'local' }; } workspaces$ = LiveData.from( new Observable(subscriber => { @@ -124,7 +120,7 @@ export class LocalWorkspaceFlavourProvider const emit = () => { const value = getLocalWorkspaceIds().map(id => ({ id, - flavour: WorkspaceFlavour.LOCAL, + flavour: 'local', })); if (isEqual(last, value)) return; subscriber.next(value); @@ -199,3 +195,18 @@ export class LocalWorkspaceFlavourProvider }; } } + +export class LocalWorkspaceFlavoursProvider + extends Service + implements WorkspaceFlavoursProvider +{ + constructor( + private readonly storageProvider: WorkspaceEngineStorageProvider + ) { + super(); + } + + workspaceFlavours$ = new LiveData([ + new LocalWorkspaceFlavourProvider(this.storageProvider, this.framework), + ]); +} diff --git a/packages/frontend/core/src/modules/workspace-engine/index.ts b/packages/frontend/core/src/modules/workspace-engine/index.ts index 65581eade662a..4175a499b6e4c 100644 --- a/packages/frontend/core/src/modules/workspace-engine/index.ts +++ b/packages/frontend/core/src/modules/workspace-engine/index.ts @@ -1,19 +1,19 @@ import { type Framework, GlobalState, - WorkspaceFlavourProvider, + WorkspaceFlavoursProvider, } from '@toeverything/infra'; import { ServersService } from '../cloud/services/servers'; import { DesktopApiService } from '../desktop-api'; -import { CloudWorkspaceFlavourProviderService } from './impls/cloud'; +import { CloudWorkspaceFlavoursProvider } from './impls/cloud'; import { IndexedDBBlobStorage } from './impls/engine/blob-indexeddb'; import { SqliteBlobStorage } from './impls/engine/blob-sqlite'; import { IndexedDBDocStorage } from './impls/engine/doc-indexeddb'; import { SqliteDocStorage } from './impls/engine/doc-sqlite'; import { LOCAL_WORKSPACE_LOCAL_STORAGE_KEY, - LocalWorkspaceFlavourProvider, + LocalWorkspaceFlavoursProvider, } from './impls/local'; import { WorkspaceEngineStorageProvider } from './providers/engine'; @@ -21,17 +21,14 @@ export { CloudBlobStorage } from './impls/engine/blob-cloud'; export function configureBrowserWorkspaceFlavours(framework: Framework) { framework - .impl(WorkspaceFlavourProvider('LOCAL'), LocalWorkspaceFlavourProvider, [ + .impl(WorkspaceFlavoursProvider('LOCAL'), LocalWorkspaceFlavoursProvider, [ WorkspaceEngineStorageProvider, ]) - .service(CloudWorkspaceFlavourProviderService, [ + .impl(WorkspaceFlavoursProvider('CLOUD'), CloudWorkspaceFlavoursProvider, [ GlobalState, WorkspaceEngineStorageProvider, ServersService, - ]) - .impl(WorkspaceFlavourProvider('CLOUD'), p => - p.get(CloudWorkspaceFlavourProviderService) - ); + ]); } export function configureIndexedDBWorkspaceEngineStorageProvider( diff --git a/packages/frontend/core/src/utils/first-app-data.ts b/packages/frontend/core/src/utils/first-app-data.ts index 57d89fd5b5d9a..57c977d9b5c16 100644 --- a/packages/frontend/core/src/utils/first-app-data.ts +++ b/packages/frontend/core/src/utils/first-app-data.ts @@ -1,6 +1,5 @@ import { DebugLogger } from '@affine/debug'; import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant'; -import { WorkspaceFlavour } from '@affine/env/workspace'; import onboardingUrl from '@affine/templates/onboarding.zip'; import { ZipTransformer } from '@blocksuite/affine/blocks'; import type { WorkspacesService } from '@toeverything/infra'; @@ -8,7 +7,7 @@ import { DocsService } from '@toeverything/infra'; export async function buildShowcaseWorkspace( workspacesService: WorkspacesService, - flavour: WorkspaceFlavour, + flavour: string, workspaceName: string ) { const meta = await workspacesService.create(flavour, async docCollection => { @@ -48,7 +47,7 @@ export async function createFirstAppData(workspacesService: WorkspacesService) { localStorage.setItem('is-first-open', 'false'); const { meta, defaultDocId } = await buildShowcaseWorkspace( workspacesService, - WorkspaceFlavour.LOCAL, + 'local', DEFAULT_WORKSPACE_NAME ); logger.info('create first workspace', defaultDocId); diff --git a/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql b/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql index e76587f8bcf6f..eb1a4f193c4df 100644 --- a/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql +++ b/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql @@ -2,4 +2,5 @@ fragment CredentialsRequirement on CredentialsRequirementType { password { ...PasswordLimits } + email } diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index 07288ef8fe6b6..caabde38987ba 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -17,6 +17,7 @@ fragment CredentialsRequirement on CredentialsRequirementType { password { ...PasswordLimits } + email }` export const adminServerConfigQuery = { id: 'adminServerConfigQuery' as const, diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 2ff409632e802..99172578249ae 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -212,6 +212,7 @@ export interface CreateUserInput { export interface CredentialsRequirementType { __typename?: 'CredentialsRequirementType'; + email: Scalars['Boolean']['output']; password: PasswordLimitsType; } @@ -1302,6 +1303,7 @@ export type AdminServerConfigQuery = { availableUserFeatures: Array; credentialsRequirement: { __typename?: 'CredentialsRequirementType'; + email: boolean; password: { __typename?: 'PasswordLimitsType'; minLength: number; @@ -1500,6 +1502,7 @@ export type ForkCopilotSessionMutation = { export type CredentialsRequirementFragment = { __typename?: 'CredentialsRequirementType'; + email: boolean; password: { __typename?: 'PasswordLimitsType'; minLength: number; @@ -2178,6 +2181,7 @@ export type ServerConfigQuery = { initialized: boolean; credentialsRequirement: { __typename?: 'CredentialsRequirementType'; + email: boolean; password: { __typename?: 'PasswordLimitsType'; minLength: number; diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 47ed394c98389..8d36f8bea8563 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,24 +1,24 @@ { - "ar": 75, + "ar": 74, "ca": 5, "da": 6, "de": 28, "el-GR": 0, "en": 100, - "es-AR": 14, + "es-AR": 13, "es-CL": 15, "es": 13, - "fr": 67, + "fr": 66, "hi": 2, "it-IT": 1, "it": 1, - "ja": 100, - "ko": 79, + "ja": 99, + "ko": 78, "pl": 0, - "pt-BR": 86, + "pt-BR": 85, "ru": 73, "sv-SE": 4, "ur": 3, - "zh-Hans": 100, - "zh-Hant": 100 + "zh-Hans": 99, + "zh-Hant": 99 } \ No newline at end of file diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 5a59d5051e3ce..7537f5b6c1572 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -284,6 +284,11 @@ "com.affine.auth.sign.email.placeholder": "Enter your email address", "com.affine.auth.sign.in": "Sign in", "com.affine.auth.sign.in.sent.email.subtitle": "Confirm your email", + "com.affine.auth.sign.add-selfhosted": "Connect to a Self-Hosted Instance", + "com.affine.auth.sign.add-selfhosted.baseurl": "Server URL", + "com.affine.auth.sign.add-selfhosted.connect-button": "Connect", + "com.affine.auth.sign.add-selfhosted.error": "Unable to connect to the server.", + "com.affine.auth.sign.add-selfhosted.description": "The Self-Hosted instance is not hosted or deployed by AFFiNE. Your data will be stored on these instances. <1>Learn more about Self-Host details.", "com.affine.auth.sign.message": "By clicking “Continue with Google/Email” above, you acknowledge that you agree to AFFiNE's <1>Terms of Conditions and <3>Privacy Policy.", "com.affine.auth.sign.policy": "Privacy policy", "com.affine.auth.sign.sent.email.message.end": " You can click the link to create an account automatically.", @@ -1241,6 +1246,8 @@ "com.affine.settings.workspace.experimental-features.enable-mobile-linked-doc-menu.description": "Enables the mobile linked doc menu.", "com.affine.settings.workspace.experimental-features.enable-snapshot-import-export.name": "Enable Snapshot Import Export", "com.affine.settings.workspace.experimental-features.enable-snapshot-import-export.description": "Once enabled, users can import and export blocksuite snapshots.", + "com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.name": "Multiple Cloud Servers", + "com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.description": "Once enabled, users can connect to selfhosted cloud servers.", "com.affine.settings.workspace.not-owner": "Only an owner can edit the workspace avatar and name. Changes will be shown for everyone.", "com.affine.settings.workspace.preferences": "Preference", "com.affine.settings.workspace.state.local": "Local", diff --git a/yarn.lock b/yarn.lock index 4add4fcb80f29..b9cb22c5eb2c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -526,6 +526,7 @@ __metadata: "@sentry/esbuild-plugin": "npm:^2.16.1" "@sentry/react": "npm:^8.0.0" "@toeverything/infra": "workspace:*" + "@types/set-cookie-parser": "npm:^2.4.10" "@types/uuid": "npm:^10.0.0" "@vitejs/plugin-react-swc": "npm:^3.6.0" async-call-rpc: "npm:^6.4.2" @@ -548,6 +549,7 @@ __metadata: react-router-dom: "npm:^6.22.3" rxjs: "npm:^7.8.1" semver: "npm:^7.6.0" + set-cookie-parser: "npm:^2.7.1" tree-kill: "npm:^1.2.2" ts-node: "npm:^10.9.2" uuid: "npm:^11.0.0" @@ -14091,6 +14093,15 @@ __metadata: languageName: node linkType: hard +"@types/set-cookie-parser@npm:^2.4.10": + version: 2.4.10 + resolution: "@types/set-cookie-parser@npm:2.4.10" + dependencies: + "@types/node": "npm:*" + checksum: 10/105cc90c7d7deeb344858f720b58bd137356586545ac00d1a448e050bfcc0f385553ff26bc9c674bd8c2e953a458149eadb1945ee3d1eee81e6c0656236ebc0a + languageName: node + linkType: hard + "@types/shimmer@npm:^1.0.2, @types/shimmer@npm:^1.2.0": version: 1.2.0 resolution: "@types/shimmer@npm:1.2.0" @@ -29861,6 +29872,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.7.1": + version: 2.7.1 + resolution: "set-cookie-parser@npm:2.7.1" + checksum: 10/c92b1130032693342bca13ea1b1bc93967ab37deec4387fcd8c2a843c0ef2fd9a9f3df25aea5bb3976cd05a91c2cf4632dd6164d6e1814208fb7d7e14edd42b4 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.1": version: 1.2.2 resolution: "set-function-length@npm:1.2.2"