diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d6e049f0c0..56133dd236 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-cmark", "state" : { - "revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53", - "version" : "0.5.0" + "revision" : "b022b08312decdc46585e0b3440d97f6f22ef703", + "version" : "0.6.0" } }, { diff --git a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9fe2607f80..ba7751279b 100644 --- a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-cmark", "state" : { - "revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53", - "version" : "0.5.0" + "revision" : "b022b08312decdc46585e0b3440d97f6f22ef703", + "version" : "0.6.0" } }, { diff --git a/packages/frontend/apps/ios/App/Podfile.lock b/packages/frontend/apps/ios/App/Podfile.lock index c37bda795a..d2f7595d60 100644 --- a/packages/frontend/apps/ios/App/Podfile.lock +++ b/packages/frontend/apps/ios/App/Podfile.lock @@ -1,14 +1,14 @@ PODS: - Capacitor (7.2.0): - CapacitorCordova - - CapacitorApp (7.0.0): + - CapacitorApp (7.0.1): - Capacitor - - CapacitorBrowser (7.0.0): + - CapacitorBrowser (7.0.1): - Capacitor - CapacitorCordova (7.2.0) - - CapacitorHaptics (7.0.0): + - CapacitorHaptics (7.0.1): - Capacitor - - CapacitorKeyboard (7.0.0): + - CapacitorKeyboard (7.0.1): - Capacitor - CryptoSwift (1.8.3) @@ -41,11 +41,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Capacitor: 106e7a4205f4618d582b886a975657c61179138d - CapacitorApp: 45cb7cbef4aa380b9236fd6980033eb5cde6fcd2 - CapacitorBrowser: 352a66541b15ceadae1d703802b11979023705e3 + CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39 + CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06 CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f - CapacitorHaptics: 1fba3e460e7614349c6d5f868b1fccdc5c87b66d - CapacitorKeyboard: 2c26c6fccde35023c579fc37d4cae6326d5e6343 + CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3 + CapacitorKeyboard: 969647d0ca2e5c737d7300088e2517aa832434e2 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 PODFILE CHECKSUM: bd61c17ff51f31ae55ec8dc579da83fda7bb51cb diff --git a/packages/frontend/apps/ios/src/app.tsx b/packages/frontend/apps/ios/src/app.tsx index 571812b379..30e71f0c13 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -337,6 +337,7 @@ CapacitorApp.addListener('appUrlOpen', ({ url }) => { if (urlObj.hostname === 'authentication') { const method = urlObj.searchParams.get('method'); const payload = JSON.parse(urlObj.searchParams.get('payload') ?? 'false'); + const serverBaseUrl = urlObj.searchParams.get('server'); if ( !method || @@ -347,9 +348,18 @@ CapacitorApp.addListener('appUrlOpen', ({ url }) => { return; } - const authService = frameworkProvider + let authService = frameworkProvider .get(DefaultServerService) .server.scope.get(AuthService); + + if (serverBaseUrl) { + const serversService = frameworkProvider.get(ServersService); + const server = serversService.getServerByBaseUrl(serverBaseUrl); + if (server) { + authService = server.scope.get(AuthService); + } + } + if (method === 'oauth') { authService .signInOauth(payload.code, payload.state, payload.provider) diff --git a/packages/frontend/core/src/mobile/components/sign-in/index.tsx b/packages/frontend/core/src/mobile/components/sign-in/index.tsx index 0d16f2daa6..4d8761c723 100644 --- a/packages/frontend/core/src/mobile/components/sign-in/index.tsx +++ b/packages/frontend/core/src/mobile/components/sign-in/index.tsx @@ -1,4 +1,4 @@ -import { SignInPanel } from '@affine/core/components/sign-in'; +import { SignInPanel, type SignInStep } from '@affine/core/components/sign-in'; import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session'; import { useCallback } from 'react'; @@ -7,9 +7,11 @@ import { MobileSignInLayout } from './layout'; export const MobileSignInPanel = ({ onClose, server, + initStep, }: { onClose: () => void; server?: string; + initStep?: SignInStep; }) => { const onAuthenticated = useCallback( (status: AuthSessionStatus) => { @@ -26,6 +28,7 @@ export const MobileSignInPanel = ({ onSkip={onClose} onAuthenticated={onAuthenticated} server={server} + initStep={initStep} /> ); diff --git a/packages/frontend/core/src/mobile/components/workspace-selector/menu.css.ts b/packages/frontend/core/src/mobile/components/workspace-selector/menu.css.ts index ff00fadeae..f5573db7cb 100644 --- a/packages/frontend/core/src/mobile/components/workspace-selector/menu.css.ts +++ b/packages/frontend/core/src/mobile/components/workspace-selector/menu.css.ts @@ -19,6 +19,7 @@ export const divider = style({ display: 'flex', alignItems: 'center', position: 'relative', + flexShrink: 0, ':before': { content: '""', width: '100%', @@ -38,6 +39,11 @@ export const head = style([ color: cssVarV2('text/primary'), }, ]); +export const headActions = style({ + display: 'flex', + alignItems: 'center', + gap: 14, +}); export const body = style({ overflowY: 'auto', flexShrink: 0, @@ -79,3 +85,30 @@ export const wsName = style([ textAlign: 'left', }, ]); + +export const serverInfo = style({ + padding: '6px 20px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', +}); +export const serverName = style([ + footnoteRegular, + { + color: cssVarV2.text.secondary, + flexShrink: 0, + }, +]); +export const serverAccount = style([ + serverName, + { + flexShrink: 1, + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + }, +]); +export const spaceX = style({ + width: 0, + flex: 1, +}); 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 bcc58ed3a3..0229a5094e 100644 --- a/packages/frontend/core/src/mobile/components/workspace-selector/menu.tsx +++ b/packages/frontend/core/src/mobile/components/workspace-selector/menu.tsx @@ -1,26 +1,37 @@ -import { IconButton } from '@affine/component'; +import { Divider, IconButton, Menu, MenuItem } from '@affine/component'; +import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info'; import { WorkspaceAvatar } from '@affine/core/components/workspace-avatar'; +import { + type AuthAccountInfo, + AuthService, + type Server, + ServersService, +} from '@affine/core/modules/cloud'; +import { GlobalDialogService } from '@affine/core/modules/dialogs'; +import { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import { GlobalContextService } from '@affine/core/modules/global-context'; import { type WorkspaceMetadata, - WorkspaceService, WorkspacesService, } from '@affine/core/modules/workspace'; -import { CloseIcon, CollaborationIcon } from '@blocksuite/icons/rc'; +import { useI18n } from '@affine/i18n'; import { - useLiveData, - useService, - useServiceOptional, -} from '@toeverything/infra'; + AccountIcon, + CloseIcon, + CollaborationIcon, + DeleteIcon, + MoreHorizontalIcon, + SelfhostIcon, + SignOutIcon, +} from '@blocksuite/icons/rc'; +import { FrameworkScope, useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; import { type HTMLAttributes, useCallback, useMemo } from 'react'; import * as styles from './menu.css'; -const filterByFlavour = (workspaces: WorkspaceMetadata[], flavour: string) => - workspaces.filter(ws => flavour === ws.flavour); - const WorkspaceItem = ({ workspace, className, @@ -49,82 +60,306 @@ const WorkspaceItem = ({ ); }; -const WorkspaceList = ({ - list, - title, - onClose, -}: { - title: string; - list: WorkspaceMetadata[]; - onClose?: () => void; -}) => { - const currentWorkspace = useServiceOptional(WorkspaceService)?.workspace; +interface WorkspaceListProps { + items: WorkspaceMetadata[]; + onClick: (workspace: WorkspaceMetadata) => void; + onSettingClick?: (workspace: WorkspaceMetadata) => void; + onEnableCloudClick?: (meta: WorkspaceMetadata) => void; +} +export const WorkspaceList = (props: WorkspaceListProps) => { + const workspaceList = props.items; - const { jumpToPage } = useNavigateHelper(); - const toggleWorkspace = useCallback( - (id: string) => { - if (id !== currentWorkspace?.id) { - jumpToPage(id, 'home'); - } - onClose?.(); - }, - [currentWorkspace?.id, jumpToPage, onClose] + return workspaceList.map(item => ( + props.onClick(item)} + /> + )); +}; + +const WorkspaceServerInfo = ({ + server, + name, + account, + accountStatus, + onDeleteServer, + onSignOut, + onSignIn, +}: { + server: string; + name: string; + account?: AuthAccountInfo | null; + accountStatus?: 'authenticated' | 'unauthenticated'; + onDeleteServer?: () => void; + onSignOut?: () => void; + onSignIn?: () => void; +}) => { + const t = useI18n(); + const isCloud = server !== 'local'; + + const menuItems = useMemo( + () => + [ + server !== 'affine-cloud' && server !== 'local' && ( + } + type="danger" + key="delete-server" + onClick={onDeleteServer} + > + {t['com.affine.server.delete']()} + + ), + accountStatus === 'authenticated' && ( + } + key="sign-out" + onClick={onSignOut} + type="danger" + > + {t['Sign out']()} + + ), + accountStatus === 'unauthenticated' && ( + } + key="sign-in" + onClick={onSignIn} + > + {t['Sign in']()} + + ), + ].filter(Boolean), + [accountStatus, onDeleteServer, onSignIn, onSignOut, server, t] ); - if (!list.length) return null; + return ( +
+
{name}
+ {isCloud ? ( +
+ - {account ? account.email : 'Not signed in'} +
+ ) : null} +
+ {menuItems.length ? ( + + } /> + + ) : null} +
+ ); +}; + +const LocalWorkspaces = ({ + workspaces, + onClickWorkspace, + onClickWorkspaceSetting, + onClickEnableCloud, +}: { + workspaces: WorkspaceMetadata[]; + onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void; + onClickWorkspaceSetting?: (workspaceMetadata: WorkspaceMetadata) => void; + onClickEnableCloud?: (meta: WorkspaceMetadata) => void; +}) => { + const t = useI18n(); + if (workspaces.length === 0) { + return null; + } + return ( + <> + + + + ); +}; + +const CloudWorkSpaceList = ({ + server, + workspaces, + onClickWorkspace, + onClickEnableCloud, +}: { + server: Server; + workspaces: WorkspaceMetadata[]; + onClickWorkspace: (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 ( <> -
{title}
-
    - {list.map(ws => ( - toggleWorkspace?.(ws.id)} - /> - ))} -
+ + ); }; +const AddServer = () => { + const globalDialogService = useService(GlobalDialogService); + const featureFlagService = useService(FeatureFlagService); + const enableMultipleServer = useLiveData( + featureFlagService.flags.enable_multiple_cloud_servers.$ + ); + + const onAddServer = useCallback(() => { + globalDialogService.open('sign-in', { step: 'addSelfhosted' }); + }, [globalDialogService]); + + if (!enableMultipleServer) { + return null; + } + return } />; +}; + export const SelectorMenu = ({ onClose }: { onClose?: () => void }) => { const workspacesService = useService(WorkspacesService); const workspaces = useLiveData(workspacesService.list.workspaces$); + const serversService = useService(ServersService); + const { jumpToPage } = useNavigateHelper(); + + const servers = useLiveData(serversService.servers$); + const affineCloudServer = useMemo( + () => servers.find(s => s.id === 'affine-cloud') as Server, + [servers] + ); + const selfhostServers = useMemo( + () => servers.filter(s => s.id !== 'affine-cloud'), + [servers] + ); - // TODO: support selfhosted const cloudWorkspaces = useMemo( - () => filterByFlavour(workspaces, 'affine-cloud'), + () => + workspaces.filter( + ({ flavour }) => flavour !== 'local' + ) as WorkspaceMetadata[], [workspaces] ); const localWorkspaces = useMemo( - () => filterByFlavour(workspaces, 'local'), + () => + workspaces.filter( + ({ flavour }) => flavour === 'local' + ) as WorkspaceMetadata[], [workspaces] ); + const handleClickWorkspace = useCallback( + (workspaceMetadata: WorkspaceMetadata) => { + const id = workspaceMetadata.id; + if (id !== currentWorkspace?.id) { + jumpToPage(id, 'home'); + } + onClose?.(); + }, + [onClose, jumpToPage] + ); + return (
Workspace - } /> +
+ + } /> +
- - {cloudWorkspaces.length && localWorkspaces.length ? ( -
- ) : null} - + flavour === affineCloudServer.id + )} + onClickWorkspace={handleClickWorkspace} + /> + + {(localWorkspaces.length > 0 || selfhostServers.length > 0) && ( + + )} + + {selfhostServers.length > 0 && } + + {/* 3. selfhost */} + {selfhostServers.map((server, index) => ( + + flavour === server.id + )} + onClickWorkspace={handleClickWorkspace} + /> + {index !== selfhostServers.length - 1 && } + + ))}
); diff --git a/packages/frontend/core/src/mobile/dialogs/sign-in/index.tsx b/packages/frontend/core/src/mobile/dialogs/sign-in/index.tsx index 78be0e2918..e50fd8a86d 100644 --- a/packages/frontend/core/src/mobile/dialogs/sign-in/index.tsx +++ b/packages/frontend/core/src/mobile/dialogs/sign-in/index.tsx @@ -1,4 +1,5 @@ import { IconButton, Modal, SafeArea } from '@affine/component'; +import type { SignInStep } from '@affine/core/components/sign-in'; import type { DialogComponentProps, GLOBAL_DIALOG_SCHEMA, @@ -11,6 +12,7 @@ import { MobileSignInPanel } from '../../components/sign-in'; export const SignInDialog = ({ close, server: initialServerBaseUrl, + step, }: DialogComponentProps) => { return ( - + } style={{ borderRadius: 8, padding: 4 }} - onClick={() => close()} + onClick={e => { + e.stopPropagation(); + close(); + }} /> diff --git a/packages/frontend/core/src/modules/feature-flag/constant.ts b/packages/frontend/core/src/modules/feature-flag/constant.ts index 16ee091c64..9ab56f0990 100644 --- a/packages/frontend/core/src/modules/feature-flag/constant.ts +++ b/packages/frontend/core/src/modules/feature-flag/constant.ts @@ -195,7 +195,7 @@ export const AFFINE_FLAGS = { description: 'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.description', configurable: false, - defaultState: isDesktopEnvironment, + defaultState: isDesktopEnvironment || BUILD_CONFIG.isIOS, }, enable_mobile_edgeless_editing: { category: 'affine',