refactor: remove legacy cloud (#2987)

This commit is contained in:
Alex Yang
2023-07-03 22:29:37 +08:00
committed by GitHub
parent 3d0a907b49
commit a5d2fafad6
87 changed files with 148 additions and 6383 deletions

View File

@@ -5,14 +5,10 @@ import {
} from '@affine/component/share-menu'; } from '@affine/component/share-menu';
import { ShareMenu } from '@affine/component/share-menu'; import { ShareMenu } from '@affine/component/share-menu';
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import {
PermissionType,
WorkspaceType,
} from '@affine/env/workspace/legacy-cloud';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { expect } from '@storybook/jest'; import { expect } from '@storybook/jest';
@@ -59,13 +55,10 @@ const localWorkspace: LocalWorkspace = {
blockSuiteWorkspace, blockSuiteWorkspace,
}; };
const affineWorkspace: AffineLegacyCloudWorkspace = { const affineWorkspace: AffineCloudWorkspace = {
id: 'test-workspace', id: 'test-workspace',
flavour: WorkspaceFlavour.AFFINE, flavour: WorkspaceFlavour.AFFINE_CLOUD,
blockSuiteWorkspace, blockSuiteWorkspace,
public: false,
type: WorkspaceType.Normal,
permission: PermissionType.Owner,
}; };
async function unimplemented() { async function unimplemented() {

View File

@@ -1,92 +0,0 @@
import { Unreachable } from '@affine/env/constant';
import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { affineApis } from '@affine/workspace/affine/shared';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { assertExists } from '@blocksuite/store';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { workspacesAtom } from '../../atoms';
type Query = (typeof QueryKey)[keyof typeof QueryKey];
export const fetcher = async (
query:
| Query
| [Query, string, boolean]
| [Query, string]
| [Query, string, string]
) => {
if (Array.isArray(query)) {
if (query[0] === QueryKey.downloadWorkspace) {
if (typeof query[2] !== 'boolean') {
throw new Unreachable();
}
return affineApis.downloadWorkspace(query[1], query[2]);
} else if (query[0] === QueryKey.getMembers) {
return affineApis.getWorkspaceMembers({
id: query[1],
});
} else if (query[0] === QueryKey.getUserByEmail) {
if (typeof query[2] !== 'string') {
throw new Unreachable();
}
return affineApis.getUserByEmail({
workspace_id: query[1],
email: query[2],
});
} else if (query[0] === QueryKey.getImage) {
const workspaceId = query[1];
const key = query[2];
if (typeof key !== 'string') {
throw new TypeError('key must be a string');
}
const workspaces = await rootStore.get(workspacesAtom);
const workspace = workspaces.find(({ id }) => id === workspaceId);
assertExists(workspace);
const storage = await workspace.blockSuiteWorkspace.blobs;
if (!storage) {
return null;
}
return storage.get(key);
} else if (query[0] === QueryKey.acceptInvite) {
const invitingCode = query[1];
if (typeof invitingCode !== 'string') {
throw new TypeError('invitingCode must be a string');
}
return affineApis.acceptInviting({
invitingCode,
});
}
} else {
if (query === QueryKey.getWorkspaces) {
return affineApis.getWorkspaces().then(workspaces => {
return workspaces.map(workspace => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspace.id,
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
}
);
const remWorkspace: AffineLegacyCloudWorkspace = {
...workspace,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,
};
return remWorkspace;
});
});
}
return (affineApis as any)[query]();
}
};
export const QueryKey = {
acceptInvite: 'acceptInvite',
getImage: 'getImage',
getWorkspaces: 'getWorkspaces',
downloadWorkspace: 'downloadWorkspace',
getMembers: 'getMembers',
getUserByEmail: 'getUserByEmail',
} as const;

View File

@@ -1,380 +0,0 @@
/**
* This file has deprecated because we do not maintain legacy affine cloud,
* please use new affine cloud instead.
*/
import { initEmptyPage } from '@affine/env/blocksuite';
import { AFFINE_STORAGE_KEY, PageNotFoundError } from '@affine/env/constant';
import type {
AffineDownloadProvider,
AffineLegacyCloudWorkspace,
LocalIndexedDBDownloadProvider,
} from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import {
LoadPriority,
ReleaseType,
WorkspaceFlavour,
} from '@affine/env/workspace';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
clearLoginStorage,
getLoginStorage,
isExpired,
parseIdToken,
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import { affineApis, affineAuth } from '@affine/workspace/affine/shared';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { createAffineDownloadProvider } from '@affine/workspace/providers';
import {
cleanupWorkspace,
createEmptyBlockSuiteWorkspace,
} from '@affine/workspace/utils';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { createJSONStorage } from 'jotai/utils';
import type { PropsWithChildren, ReactElement } from 'react';
import { Suspense, useEffect } from 'react';
import { mutate } from 'swr';
import { z } from 'zod';
import { PageLoading } from '../../components/pure/loading';
import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh-auth-token';
import { BlockSuiteWorkspace } from '../../shared';
import { toast } from '../../utils';
import {
BlockSuitePageList,
NewWorkspaceSettingDetail,
PageDetailEditor,
WorkspaceHeader,
WorkspaceSettingDetail,
} from '../shared';
import { QueryKey } from './fetcher';
const storage = createJSONStorage(() => localStorage);
const schema = z.object({
id: z.string(),
type: z.number(),
public: z.boolean(),
permission: z.number(),
});
const getPersistenceAllWorkspace = () => {
const items = storage.getItem(AFFINE_STORAGE_KEY, []);
const allWorkspaces: AffineLegacyCloudWorkspace[] = [];
if (
Array.isArray(items) &&
items.every(item => schema.safeParse(item).success)
) {
allWorkspaces.push(
...items.map((item: z.infer<typeof schema>) => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
item.id,
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
}
);
const affineWorkspace: AffineLegacyCloudWorkspace = {
...item,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,
};
return affineWorkspace;
})
);
}
return allWorkspaces;
};
function AuthContext({ children }: PropsWithChildren): ReactElement {
const login = useAffineRefreshAuthToken();
useEffect(() => {
if (!login) {
console.warn('No login, redirecting to local workspace page...');
}
}, [login]);
if (!login) {
return <PageLoading />;
}
return <>{children}</>;
}
export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
releaseType: ReleaseType.STABLE,
flavour: WorkspaceFlavour.AFFINE,
loadPriority: LoadPriority.HIGH,
Events: {
'workspace:access': async () => {
if (!runtimeConfig.enableLegacyCloud) {
console.warn('Legacy cloud is disabled');
return;
}
const response = await affineAuth.generateToken(SignMethod.Google);
if (response) {
setLoginStorage(response);
const user = parseIdToken(response.token);
rootStore.set(currentAffineUserAtom, user);
} else {
toast('Login failed');
}
},
'workspace:revoke': async () => {
if (!runtimeConfig.enableLegacyCloud) {
console.warn('Legacy cloud is disabled');
return;
}
await rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
workspaces.filter(
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
)
);
storage.removeItem(AFFINE_STORAGE_KEY);
clearLoginStorage();
rootStore.set(currentAffineUserAtom, null);
},
},
CRUD: {
create: async blockSuiteWorkspace => {
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
blockSuiteWorkspace.doc
);
const { id } = await affineApis.createWorkspace(binary);
// fixme: syncing images
const newWorkspaceId = id;
await new Promise(resolve => setTimeout(resolve, 1000));
const blobManager = blockSuiteWorkspace.blobs;
for (const id of await blobManager.list()) {
const blob = await blobManager.get(id);
if (blob) {
await affineApis.uploadBlob(
newWorkspaceId,
await blob.arrayBuffer(),
blob.type
);
}
}
{
const bs = createEmptyBlockSuiteWorkspace(id, WorkspaceFlavour.AFFINE, {
workspaceApis: affineApis,
});
// fixme:
// force to download workspace binary
// to make sure the workspace is synced
const provider = createAffineDownloadProvider(bs.id, bs.doc, {
awareness: bs.awarenessStore.awareness,
}) as AffineDownloadProvider;
const indexedDBProvider = createIndexedDBDownloadProvider(
bs.id,
bs.doc,
{
awareness: bs.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
indexedDBProvider.sync();
await indexedDBProvider.whenReady;
provider.disconnect();
}
await mutate(matcher => matcher === QueryKey.getWorkspaces);
// refresh the local storage
await AffineAdapter.CRUD.list();
return id;
},
delete: async workspace => {
const items = storage.getItem(AFFINE_STORAGE_KEY, []);
if (
Array.isArray(items) &&
items.every(item => schema.safeParse(item).success)
) {
storage.setItem(
AFFINE_STORAGE_KEY,
items.filter(item => item.id !== workspace.id)
);
}
await affineApis.deleteWorkspace({
id: workspace.id,
});
await mutate(matcher => matcher === QueryKey.getWorkspaces);
},
get: async workspaceId => {
// fixme(himself65): rewrite the auth logic
try {
const loginStorage = getLoginStorage();
if (
loginStorage == null ||
isExpired(parseIdToken(loginStorage.token))
) {
rootStore.set(currentAffineUserAtom, null);
storage.removeItem(AFFINE_STORAGE_KEY);
cleanupWorkspace(WorkspaceFlavour.AFFINE);
return null;
}
const workspaces: AffineLegacyCloudWorkspace[] =
await AffineAdapter.CRUD.list();
return (
workspaces.find(workspace => workspace.id === workspaceId) ?? null
);
} catch (e) {
const workspaces = getPersistenceAllWorkspace();
return (
workspaces.find(workspace => workspace.id === workspaceId) ?? null
);
}
},
list: async () => {
const allWorkspaces = getPersistenceAllWorkspace();
const loginStorage = getLoginStorage();
// fixme(himself65): rewrite the auth logic
try {
if (
loginStorage == null ||
isExpired(parseIdToken(loginStorage.token))
) {
rootStore.set(currentAffineUserAtom, null);
storage.removeItem(AFFINE_STORAGE_KEY);
return [];
}
} catch (e) {
storage.removeItem(AFFINE_STORAGE_KEY);
return [];
}
try {
const workspaces = await affineApis.getWorkspaces().then(workspaces => {
return workspaces.map(workspace => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspace.id,
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
}
);
const dump = workspaces.map(workspace => {
return {
id: workspace.id,
type: workspace.type,
public: workspace.public,
permission: workspace.permission,
} satisfies z.infer<typeof schema>;
});
const old = storage.getItem(AFFINE_STORAGE_KEY, []);
if (
Array.isArray(old) &&
old.every(item => schema.safeParse(item).success)
) {
const data = [...dump];
old.forEach((item: z.infer<typeof schema>) => {
const has = dump.find(dump => dump.id === item.id);
if (!has) {
data.push(item);
}
});
storage.setItem(AFFINE_STORAGE_KEY, [...data]);
}
const affineWorkspace: AffineLegacyCloudWorkspace = {
...workspace,
flavour: WorkspaceFlavour.AFFINE,
blockSuiteWorkspace,
};
return affineWorkspace;
});
});
workspaces.forEach(workspace => {
const idx = allWorkspaces.findIndex(({ id }) => id === workspace.id);
if (idx !== -1) {
allWorkspaces.splice(idx, 1, workspace);
} else {
allWorkspaces.push(workspace);
}
});
// only save data when login in
const dump = allWorkspaces.map(workspace => {
return {
id: workspace.id,
type: workspace.type,
public: workspace.public,
permission: workspace.permission,
} satisfies z.infer<typeof schema>;
});
storage.setItem(AFFINE_STORAGE_KEY, [...dump]);
} catch (e) {
console.error('fetch affine workspaces failed', e);
}
return [...allWorkspaces];
},
},
UI: {
Provider: ({ children }) => {
return (
<Suspense fallback={<PageLoading />}>
<AuthContext>{children}</AuthContext>
</Suspense>
);
},
Header: WorkspaceHeader,
PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
throw new PageNotFoundError(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
}
return (
<>
<PageDetailEditor
pageId={currentPageId}
workspace={currentWorkspace}
onInit={initEmptyPage}
onLoad={onLoadEditor}
/>
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
return (
<BlockSuitePageList
collection={collection}
listType="all"
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
);
},
SettingsDetail: ({
currentWorkspace,
onChangeTab,
currentTab,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<WorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
onChangeTab={onChangeTab}
currentTab={currentTab}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
NewSettingsDetail: ({
currentWorkspace,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<NewWorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
},
};

View File

@@ -25,7 +25,6 @@ import {
NewWorkspaceSettingDetail, NewWorkspaceSettingDetail,
PageDetailEditor, PageDetailEditor,
WorkspaceHeader, WorkspaceHeader,
WorkspaceSettingDetail,
} from '../shared'; } from '../shared';
const logger = new DebugLogger('use-create-first-workspace'); const logger = new DebugLogger('use-create-first-workspace');
@@ -105,23 +104,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
/> />
); );
}, },
SettingsDetail: ({
currentWorkspace,
onChangeTab,
currentTab,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<WorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
onChangeTab={onChangeTab}
currentTab={currentTab}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
NewSettingsDetail: ({ NewSettingsDetail: ({
currentWorkspace, currentWorkspace,
onDeleteWorkspace, onDeleteWorkspace,

View File

@@ -1,12 +1,5 @@
import { lazy } from 'react'; import { lazy } from 'react';
// export { WorkspaceSettingDetail as NewWorkspaceSettingDetail } from '../components/affine/new-workspace-setting-detail';
export const WorkspaceSettingDetail = lazy(() =>
import('../components/affine/workspace-setting-detail').then(
({ WorkspaceSettingDetail }) => ({
default: WorkspaceSettingDetail,
})
)
);
export const NewWorkspaceSettingDetail = lazy(() => export const NewWorkspaceSettingDetail = lazy(() =>
import('../components/affine/new-workspace-setting-detail').then( import('../components/affine/new-workspace-setting-detail').then(
({ WorkspaceSettingDetail }) => ({ ({ WorkspaceSettingDetail }) => ({
@@ -14,6 +7,7 @@ export const NewWorkspaceSettingDetail = lazy(() =>
}) })
) )
); );
export const BlockSuitePageList = lazy(() => export const BlockSuitePageList = lazy(() =>
import('../components/blocksuite/block-suite-page-list').then( import('../components/blocksuite/block-suite-page-list').then(
({ BlockSuitePageList }) => ({ ({ BlockSuitePageList }) => ({
@@ -21,6 +15,7 @@ export const BlockSuitePageList = lazy(() =>
}) })
) )
); );
export const PageDetailEditor = lazy(() => export const PageDetailEditor = lazy(() =>
import('../components/page-detail-editor').then(({ PageDetailEditor }) => ({ import('../components/page-detail-editor').then(({ PageDetailEditor }) => ({
default: PageDetailEditor, default: PageDetailEditor,

View File

@@ -10,7 +10,6 @@ import {
WorkspaceFlavour, WorkspaceFlavour,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { AffineAdapter } from './affine';
import { LocalAdapter } from './local'; import { LocalAdapter } from './local';
const unimplemented = () => { const unimplemented = () => {
@@ -22,7 +21,6 @@ const bypassList = async () => {
}; };
export const WorkspaceAdapters = { export const WorkspaceAdapters = {
[WorkspaceFlavour.AFFINE]: AffineAdapter,
[WorkspaceFlavour.LOCAL]: LocalAdapter, [WorkspaceFlavour.LOCAL]: LocalAdapter,
[WorkspaceFlavour.AFFINE_CLOUD]: { [WorkspaceFlavour.AFFINE_CLOUD]: {
releaseType: ReleaseType.UNRELEASED, releaseType: ReleaseType.UNRELEASED,
@@ -42,7 +40,6 @@ export const WorkspaceAdapters = {
Header: unimplemented, Header: unimplemented,
PageDetail: unimplemented, PageDetail: unimplemented,
PageList: unimplemented, PageList: unimplemented,
SettingsDetail: unimplemented,
NewSettingsDetail: unimplemented, NewSettingsDetail: unimplemented,
}, },
}, },
@@ -64,7 +61,6 @@ export const WorkspaceAdapters = {
Header: unimplemented, Header: unimplemented,
PageDetail: unimplemented, PageDetail: unimplemented,
PageList: unimplemented, PageList: unimplemented,
SettingsDetail: unimplemented,
NewSettingsDetail: unimplemented, NewSettingsDetail: unimplemented,
}, },
}, },

View File

@@ -1,68 +0,0 @@
import type { BlockSuiteFeatureFlags } from '@affine/env/global';
import type { AffinePublicWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { affineApis } from '@affine/workspace/affine/shared';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { atom } from 'jotai';
import { BlockSuiteWorkspace } from '../../shared';
function createPublicWorkspace(
workspaceId: string,
binary: ArrayBuffer,
singlePage = false
): AffinePublicWorkspace {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspaceId,
WorkspaceFlavour.AFFINE,
{
workspaceApis: affineApis,
cachePrefix: WorkspaceFlavour.PUBLIC + (singlePage ? '-single-page' : ''),
}
);
BlockSuiteWorkspace.Y.applyUpdate(
blockSuiteWorkspace.doc,
new Uint8Array(binary)
);
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
blockSuiteWorkspace.awarenessStore.setFlag(
key as keyof BlockSuiteFeatureFlags,
value
);
});
// force disable some features
blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false);
blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false);
return {
flavour: WorkspaceFlavour.PUBLIC,
id: workspaceId,
blockSuiteWorkspace,
};
}
export const publicWorkspaceIdAtom = atom<string | null>(null);
export const publicWorkspacePageIdAtom = atom<string | null>(null);
export const publicPageBlockSuiteAtom = atom<Promise<AffinePublicWorkspace>>(
async get => {
const workspaceId = get(publicWorkspaceIdAtom);
const pageId = get(publicWorkspacePageIdAtom);
if (!workspaceId || !pageId) {
throw new Error('No workspace id or page id');
}
const binary = await affineApis.downloadPublicWorkspacePage(
workspaceId,
pageId
);
return createPublicWorkspace(workspaceId, binary, true);
}
);
export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
async get => {
const workspaceId = get(publicWorkspaceIdAtom);
if (!workspaceId) {
throw new Error('No workspace id');
}
const binary = await affineApis.downloadWorkspace(workspaceId, true);
return createPublicWorkspace(workspaceId, binary, false);
}
);

View File

@@ -4,7 +4,7 @@ import type {
WorkspaceAdapter, WorkspaceAdapter,
WorkspaceRegistry, WorkspaceRegistry,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace';
import { import {
rootCurrentWorkspaceIdAtom, rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom, rootWorkspacesMetadataAtom,
@@ -26,16 +26,9 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
const flavours: string[] = Object.values(WorkspaceAdapters).map( const flavours: string[] = Object.values(WorkspaceAdapters).map(
plugin => plugin.flavour plugin => plugin.flavour
); );
const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom)) const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom)).filter(
.filter( workspace => flavours.includes(workspace.flavour)
workspace => flavours.includes(workspace.flavour) );
// TODO: remove this when we remove the legacy cloud
)
.filter(workspace =>
!runtimeConfig.enableLegacyCloud
? workspace.flavour !== WorkspaceFlavour.AFFINE
: true
);
if (jotaiWorkspaces.some(meta => !('version' in meta))) { if (jotaiWorkspaces.some(meta => !('version' in meta))) {
// wait until all workspaces have migrated to v2 // wait until all workspaces have migrated to v2
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@@ -63,7 +56,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
}) })
).then(workspaces => ).then(workspaces =>
workspaces.filter( workspaces.filter(
(workspace): workspace is WorkspaceRegistry['affine' | 'local'] => (workspace): workspace is WorkspaceRegistry['affine-cloud' | 'local'] =>
workspace !== null workspace !== null
) )
); );

View File

@@ -4,7 +4,6 @@ import type {
WorkspaceNotFoundError, WorkspaceNotFoundError,
} from '@affine/env/constant'; } from '@affine/env/constant';
import { PageNotFoundError } from '@affine/env/constant'; import { PageNotFoundError } from '@affine/env/constant';
import { RequestError } from '@affine/workspace/affine/api';
import type { NextRouter } from 'next/router'; import type { NextRouter } from 'next/router';
import type { ErrorInfo, ReactNode } from 'react'; import type { ErrorInfo, ReactNode } from 'react';
import type React from 'react'; import type React from 'react';
@@ -19,7 +18,6 @@ type AffineError =
| Unreachable | Unreachable
| WorkspaceNotFoundError | WorkspaceNotFoundError
| PageNotFoundError | PageNotFoundError
| RequestError
| Error; | Error;
interface AffineErrorBoundaryState { interface AffineErrorBoundaryState {
@@ -78,13 +76,6 @@ export class AffineErrorBoundary extends Component<
</> </>
</> </>
); );
} else if (error instanceof RequestError) {
return (
<>
<h1>Sorry.. there was an error</h1>
{error.message}
</>
);
} }
return ( return (
<> <>

View File

@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons'; import { CloseIcon } from '@blocksuite/icons';
import type React from 'react'; import type React from 'react';
import { useCurrentUser } from '../../../hooks/current/use-current-user';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style'; import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
interface EnableAffineCloudModalProps { interface EnableAffineCloudModalProps {
@@ -18,7 +17,6 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
onClose, onClose,
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const user = useCurrentUser();
return ( return (
<Modal open={open} onClose={onClose} data-testid="logout-modal"> <Modal open={open} onClose={onClose} data-testid="logout-modal">
@@ -39,7 +37,7 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
type="primary" type="primary"
onClick={onConfirm} onClick={onConfirm}
> >
{user ? t.Enable() : t['Sign in and Enable']()} {t['Sign in and Enable']()}
</StyleButton> </StyleButton>
<StyleButton shape="round" onClick={onClose}> <StyleButton shape="round" onClick={onClose}>
{t['Not now']()} {t['Not now']()}

View File

@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { type FC, useState } from 'react'; import { type FC, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
import type { AffineOfficialWorkspace } from '../../../../shared'; import type { AffineOfficialWorkspace } from '../../../../shared';
import type { WorkspaceSettingDetailProps } from '../index'; import type { WorkspaceSettingDetailProps } from '../index';
import { WorkspaceDeleteModal } from './delete'; import { WorkspaceDeleteModal } from './delete';
@@ -14,7 +13,8 @@ export const DeleteLeaveWorkspace: FC<{
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace']; onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
}> = ({ workspace, onDeleteWorkspace }) => { }> = ({ workspace, onDeleteWorkspace }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(workspace); // fixme: cloud regression
const isOwner = true;
const [showDelete, setShowDelete] = useState(false); const [showDelete, setShowDelete] = useState(false);
const [showLeave, setShowLeave] = useState(false); const [showLeave, setShowLeave] = useState(false);

View File

@@ -14,7 +14,6 @@ import type { FC } from 'react';
import type { AffineOfficialWorkspace } from '../../../shared'; import type { AffineOfficialWorkspace } from '../../../shared';
import { DeleteLeaveWorkspace } from './delete-leave-workspace'; import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { ExportPanel } from './export'; import { ExportPanel } from './export';
import { MembersPanel } from './members';
import { ProfilePanel } from './profile'; import { ProfilePanel } from './profile';
import { PublishPanel } from './publish'; import { PublishPanel } from './publish';
import { StoragePanel } from './storage'; import { StoragePanel } from './storage';
@@ -63,11 +62,6 @@ export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
onDeleteWorkspace={onDeleteWorkspace} onDeleteWorkspace={onDeleteWorkspace}
{...props} {...props}
/> />
<MembersPanel
workspace={workspace}
onDeleteWorkspace={onDeleteWorkspace}
{...props}
/>
</SettingWrapper> </SettingWrapper>
{environment.isDesktop ? ( {environment.isDesktop ? (
<SettingWrapper title={t['Storage and Export']()}> <SettingWrapper title={t['Storage and Export']()}>

View File

@@ -1,160 +0,0 @@
import { Button, IconButton, Menu, MenuItem } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { UserAvatar } from '@affine/component/user-avatar';
import { Unreachable } from '@affine/env/constant';
import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DeleteTemporarilyIcon, MoreVerticalIcon } from '@blocksuite/icons';
import type { FC } from 'react';
import React, { useCallback, useState } from 'react';
import { useMembers } from '../../../../hooks/affine/use-members';
import type { AffineOfficialWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import type { WorkspaceSettingDetailProps } from '../index';
import { fakeWrapper } from '../style.css';
import { InviteMemberModal } from './invite-member-modal';
import * as style from './style.css';
export type AffineRemoteMembersProps = WorkspaceSettingDetailProps & {
workspace: AffineLegacyCloudWorkspace;
};
export type MemberPanelProps = WorkspaceSettingDetailProps & {
workspace: AffineOfficialWorkspace;
};
const MemberList: FC<{
workspace: AffineLegacyCloudWorkspace;
}> = ({ workspace }) => {
const t = useAFFiNEI18N();
const { members, removeMember } = useMembers(workspace.id);
if (members.length) {
return null;
}
return (
<ul className={style.memberList}>
{members
.sort((b, a) => a.type - b.type)
.map(member => {
const { id, name, email, avatar_url } = {
name: '',
email: '',
avatar_url: '',
...member,
};
return (
<li className="member-list-item" key={id}>
<div className="left-col">
<UserAvatar size={36} name={name} url={avatar_url} />
<div className="user-info-wrapper">
<p className="user-name">{name}</p>
<p className="email">{email}</p>
</div>
</div>
<div className="right-col">
<div className="user-identity">
{member.accepted
? member.type !== PermissionType.Owner
? t['Member']()
: t['Owner']()
: t['Pending']()}
</div>
<Menu
content={
<>
<MenuItem
onClick={async () => {
await removeMember(Number(id));
toast(
t['Member has been removed']({
name,
})
);
}}
icon={<DeleteTemporarilyIcon />}
>
{t['Remove from workspace']()}
</MenuItem>
</>
}
placement="bottom"
disablePortal={true}
trigger="click"
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
</div>
</li>
);
})}
</ul>
);
};
export const AffineRemoteMembers: FC<AffineRemoteMembersProps> = ({
workspace,
}) => {
const t = useAFFiNEI18N();
const { members } = useMembers(workspace.id);
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
return (
<>
<SettingRow
name={`${t['Members']()} (${members.length})`}
desc={t['Members hint']()}
style={{ marginTop: '25px' }}
>
<Button
size="middle"
onClick={() => {
setIsInviteModalShow(true);
}}
>
{t['Invite']()}
</Button>
</SettingRow>
<MemberList workspace={workspace} />
<InviteMemberModal
onClose={useCallback(() => {
setIsInviteModalShow(false);
}, [])}
onInviteSuccess={useCallback(() => {
setIsInviteModalShow(false);
}, [])}
workspaceId={workspace.id}
open={isInviteModalShow}
/>
</>
);
};
export const FakeMembers: FC = () => {
const t = useAFFiNEI18N();
return (
<div className={fakeWrapper} style={{ marginTop: '25px' }}>
<SettingRow name={`${t['Members']()} (0)`} desc={t['Members hint']()}>
<Button size="middle">{t['Invite']()}</Button>
</SettingRow>
</div>
);
};
export const MembersPanel: FC<MemberPanelProps> = props => {
switch (props.workspace.flavour) {
case WorkspaceFlavour.AFFINE: {
const workspace = props.workspace as AffineLegacyCloudWorkspace;
return <AffineRemoteMembers {...props} workspace={workspace} />;
}
case WorkspaceFlavour.LOCAL: {
return <FakeMembers />;
}
}
throw new Unreachable();
};

View File

@@ -1,222 +0,0 @@
import {
Button,
Input,
Modal,
ModalCloseButton,
ModalWrapper,
MuiAvatar,
styled,
} from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EmailIcon } from '@blocksuite/icons';
import type React from 'react';
import { Suspense, useCallback, useState } from 'react';
import { useMembers } from '../../../../../hooks/affine/use-members';
import { useUsersByEmail } from '../../../../../hooks/affine/use-users-by-email';
interface LoginModalProps {
open: boolean;
onClose: () => void;
workspaceId: string;
onInviteSuccess: () => void;
}
const gmailReg =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@(gmail|example)\.(com|org)$/;
const Result: React.FC<{
workspaceId: string;
queryEmail: string;
}> = ({ workspaceId, queryEmail }) => {
const users = useUsersByEmail(workspaceId, queryEmail);
const firstUser = users?.at(0) ?? null;
if (!firstUser || !firstUser.email) {
return null;
}
return (
<Members>
<Member>
{firstUser.avatar_url ? (
<MuiAvatar src={firstUser.avatar_url}></MuiAvatar>
) : (
<MemberIcon>
<EmailIcon></EmailIcon>
</MemberIcon>
)}
<Email>{firstUser.email}</Email>
{/* <div>invited</div> */}
</Member>
</Members>
);
};
export const InviteMemberModal = ({
open,
onClose,
onInviteSuccess,
workspaceId,
}: LoginModalProps) => {
const { inviteMember } = useMembers(workspaceId);
const [email, setEmail] = useState<string>('');
const [showMemberPreview, setShowMemberPreview] = useState(false);
const t = useAFFiNEI18N();
const inputChange = useCallback((value: string) => {
setEmail(value);
}, []);
return (
<div>
<Modal open={open} onClose={onClose}>
<ModalWrapper width={460} height={236}>
<Header>
<ModalCloseButton
onClick={() => {
onClose();
setEmail('');
}}
/>
</Header>
<Content>
<ContentTitle>{t['Invite Members']()}</ContentTitle>
<InviteBox>
<Input
data-testid="invite-member-input"
width={360}
value={email}
onChange={inputChange}
onFocus={useCallback(() => {
setShowMemberPreview(true);
}, [])}
onBlur={useCallback(() => {
setShowMemberPreview(false);
}, [])}
placeholder={t['Invite placeholder']()}
/>
{showMemberPreview && gmailReg.test(email) && (
<Suspense fallback="loading...">
<Result workspaceId={workspaceId} queryEmail={email} />
</Suspense>
)}
</InviteBox>
</Content>
<Footer>
<Button
data-testid="invite-member-button"
disabled={!gmailReg.test(email)}
shape="circle"
type="primary"
style={{
width: '364px',
height: '38px',
borderRadius: '40px',
}}
onClick={async () => {
await inviteMember(email);
setEmail('');
onInviteSuccess();
}}
>
{t['Invite']()}
</Button>
</Footer>
</ModalWrapper>
</Modal>
</div>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const Footer = styled('div')({
height: '102px',
margin: '32px 0',
textAlign: 'center',
});
const InviteBox = styled('div')({
position: 'relative',
});
const Members = styled('div')(() => {
return {
position: 'absolute',
width: '100%',
background: 'var(--affine-background-primary-color)',
textAlign: 'left',
zIndex: 1,
borderRadius: '0px 10px 10px 10px',
height: '56px',
padding: '8px 12px',
input: {
'&::placeholder': {
color: 'var(--affine-placeholder-color)',
},
},
};
});
// const NoFind = styled('div')(({ theme }) => {
// return {
// color: 'var(--affine-icon-color)',
// fontSize: 'var(--affine-font-sm)',
// lineHeight: '40px',
// userSelect: 'none',
// width: '100%',
// };
// });
const Member = styled('div')(() => {
return {
color: 'var(--affine-icon-color)',
fontSize: 'var(--affine-font-sm)',
lineHeight: '40px',
userSelect: 'none',
display: 'flex',
};
});
const MemberIcon = styled('div')(() => {
return {
width: '40px',
height: '40px',
borderRadius: '50%',
color: 'var(--affine-primary-color)',
background: '#F5F5F5',
textAlign: 'center',
lineHeight: '45px',
// icon size
fontSize: '20px',
overflow: 'hidden',
img: {
width: '100%',
height: '100%',
},
};
});
const Email = styled('div')(() => {
return {
flex: '1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginLeft: '8px',
};
});

View File

@@ -1,54 +0,0 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const memberList = style({
marginTop: '12px',
});
globalStyle(`${memberList} .member-list-item`, {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
globalStyle(`${memberList} .member-list-item:not(:last-of-type)`, {
marginBottom: '8px',
});
globalStyle(`${memberList} .left-col`, {
display: 'flex',
alignItems: 'center',
width: '60%',
});
globalStyle(`${memberList} .right-col`, {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
width: '35%',
});
globalStyle(`${memberList} .user-info-wrapper`, {
flexGrow: 1,
marginLeft: '12px',
overflow: 'hidden',
});
globalStyle(`${memberList} .user-info-wrapper p`, {
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
globalStyle(`${memberList} .user-name`, {
fontSize: 'var(--affine-font-sm)',
});
globalStyle(`${memberList} .email`, {
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
});
globalStyle(`${memberList} .user-identity`, {
fontSize: 'var(--affine-font-sm)',
marginRight: '15px',
flexGrow: '1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});

View File

@@ -6,17 +6,33 @@ import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-s
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { type FC, useCallback, useState } from 'react'; import { type FC, useCallback, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
import type { AffineOfficialWorkspace } from '../../../shared'; import type { AffineOfficialWorkspace } from '../../../shared';
import { Upload } from '../../pure/file-upload'; import { Upload } from '../../pure/file-upload';
import { CameraIcon } from '../workspace-setting-detail/panel/general/icons';
import * as style from './style.css'; import * as style from './style.css';
const CameraIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.6236 4.25001C10.635 4.25001 10.6467 4.25002 10.6584 4.25002H13.3416C13.3533 4.25002 13.365 4.25001 13.3764 4.25001C13.5609 4.24995 13.7105 4.2499 13.8543 4.26611C14.5981 4.34997 15.2693 4.75627 15.6826 5.38026C15.7624 5.50084 15.83 5.63398 15.9121 5.79586C15.9173 5.80613 15.9226 5.81652 15.9279 5.82703C15.9538 5.87792 15.9679 5.90562 15.9789 5.9261C15.9832 5.9341 15.9857 5.93861 15.9869 5.94065C16.0076 5.97069 16.0435 5.99406 16.0878 5.99905L16.0849 5.99877C16.0849 5.99877 16.0907 5.99918 16.1047 5.99947C16.1286 5.99998 16.1604 6.00002 16.2181 6.00002L17.185 6.00001C17.6577 6 18.0566 5.99999 18.3833 6.02627C18.7252 6.05377 19.0531 6.11364 19.3656 6.27035C19.8402 6.50842 20.2283 6.88944 20.4723 7.36077C20.6336 7.67233 20.6951 7.99944 20.7232 8.33858C20.75 8.66166 20.75 9.05554 20.75 9.51992V16.2301C20.75 16.6945 20.75 17.0884 20.7232 17.4114C20.6951 17.7506 20.6336 18.0777 20.4723 18.3893C20.2283 18.8606 19.8402 19.2416 19.3656 19.4797C19.0531 19.6364 18.7252 19.6963 18.3833 19.7238C18.0566 19.75 17.6578 19.75 17.185 19.75H6.81497C6.34225 19.75 5.9434 19.75 5.61668 19.7238C5.27477 19.6963 4.94688 19.6364 4.63444 19.4797C4.15978 19.2416 3.77167 18.8606 3.52771 18.3893C3.36644 18.0777 3.30494 17.7506 3.27679 17.4114C3.24998 17.0884 3.24999 16.6945 3.25 16.2302V9.51987C3.24999 9.05551 3.24998 8.66164 3.27679 8.33858C3.30494 7.99944 3.36644 7.67233 3.52771 7.36077C3.77167 6.88944 4.15978 6.50842 4.63444 6.27035C4.94688 6.11364 5.27477 6.05377 5.61668 6.02627C5.9434 5.99999 6.34225 6 6.81498 6.00001L7.78191 6.00002C7.83959 6.00002 7.87142 5.99998 7.8953 5.99947C7.90607 5.99924 7.91176 5.99897 7.91398 5.99884C7.95747 5.99343 7.99267 5.9703 8.01312 5.94066C8.01429 5.93863 8.01684 5.93412 8.02113 5.9261C8.0321 5.90561 8.04622 5.87791 8.07206 5.82703C8.07739 5.81653 8.08266 5.80615 8.08787 5.79588C8.17004 5.63397 8.23759 5.50086 8.31745 5.38026C8.73067 4.75627 9.40192 4.34997 10.1457 4.26611C10.2895 4.2499 10.4391 4.24995 10.6236 4.25001ZM10.6584 5.75002C10.422 5.75002 10.3627 5.75114 10.3138 5.75666C10.0055 5.79142 9.73316 5.95919 9.56809 6.20845C9.54218 6.24758 9.51544 6.29761 9.40943 6.50633C9.40611 6.51287 9.40274 6.5195 9.39934 6.52622C9.36115 6.60161 9.31758 6.68761 9.26505 6.76694C8.9964 7.17261 8.56105 7.4354 8.08026 7.48961C7.98625 7.50021 7.89021 7.50011 7.80434 7.50003C7.79678 7.50002 7.7893 7.50002 7.78191 7.50002H6.84445C6.33444 7.50002 5.99634 7.50058 5.73693 7.52144C5.48594 7.54163 5.37478 7.57713 5.30693 7.61115C5.11257 7.70864 4.95675 7.86306 4.85983 8.05029C4.82733 8.11308 4.79194 8.21816 4.77165 8.46266C4.7506 8.71626 4.75 9.0474 4.75 9.55001V16.2C4.75 16.7026 4.7506 17.0338 4.77165 17.2874C4.79194 17.5319 4.82733 17.6369 4.85983 17.6997C4.95675 17.887 5.11257 18.0414 5.30693 18.1389C5.37478 18.1729 5.48594 18.2084 5.73693 18.2286C5.99634 18.2494 6.33444 18.25 6.84445 18.25H17.1556C17.6656 18.25 18.0037 18.2494 18.2631 18.2286C18.5141 18.2084 18.6252 18.1729 18.6931 18.1389C18.8874 18.0414 19.0433 17.887 19.1402 17.6997C19.1727 17.6369 19.2081 17.5319 19.2283 17.2874C19.2494 17.0338 19.25 16.7026 19.25 16.2V9.55001C19.25 9.0474 19.2494 8.71626 19.2283 8.46266C19.2081 8.21816 19.1727 8.11308 19.1402 8.05029C19.0433 7.86306 18.8874 7.70864 18.6931 7.61115C18.6252 7.57713 18.5141 7.54163 18.2631 7.52144C18.0037 7.50058 17.6656 7.50002 17.1556 7.50002H16.2181C16.2107 7.50002 16.2032 7.50002 16.1957 7.50003C16.1098 7.50011 16.0138 7.50021 15.9197 7.48961C15.4389 7.4354 15.0036 7.17261 14.735 6.76694C14.6824 6.68761 14.6389 6.60163 14.6007 6.52622C14.5973 6.5195 14.5939 6.51287 14.5906 6.50633C14.4846 6.29763 14.4578 6.24758 14.4319 6.20846C14.2668 5.95919 13.9945 5.79142 13.6862 5.75666C13.6373 5.75114 13.578 5.75002 13.3416 5.75002H10.6584ZM12 11C10.9303 11 10.0833 11.8506 10.0833 12.875C10.0833 13.8995 10.9303 14.75 12 14.75C13.0697 14.75 13.9167 13.8995 13.9167 12.875C13.9167 11.8506 13.0697 11 12 11ZM8.58333 12.875C8.58333 11 10.1242 9.50002 12 9.50002C13.8758 9.50002 15.4167 11 15.4167 12.875C15.4167 14.7501 13.8758 16.25 12 16.25C10.1242 16.25 8.58333 14.7501 8.58333 12.875Z"
fill="white"
/>
</svg>
);
};
export const ProfilePanel: FC<{ export const ProfilePanel: FC<{
workspace: AffineOfficialWorkspace; workspace: AffineOfficialWorkspace;
}> = ({ workspace }) => { }> = ({ workspace }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(workspace);
const [, update] = useBlockSuiteWorkspaceAvatarUrl( const [, update] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace workspace.blockSuiteWorkspace
@@ -38,22 +54,18 @@ export const ProfilePanel: FC<{
return ( return (
<div className={style.profileWrapper}> <div className={style.profileWrapper}>
<div className={style.avatarWrapper}> <div className={style.avatarWrapper}>
{isOwner ? ( <Upload
<Upload accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg" fileChange={update}
fileChange={update} data-testid="upload-avatar"
data-testid="upload-avatar" >
> <>
<> <div className="camera-icon-wrapper">
<div className="camera-icon-wrapper"> <CameraIcon />
<CameraIcon /> </div>
</div> <WorkspaceAvatar size={56} workspace={workspace} />
<WorkspaceAvatar size={56} workspace={workspace} /> </>
</> </Upload>
</Upload>
) : (
<WorkspaceAvatar size={56} workspace={workspace} />
)}
</div> </div>
<div className={style.profileHandlerWrapper}> <div className={style.profileHandlerWrapper}>
<Input <Input

View File

@@ -2,7 +2,7 @@ import { Button, FlexWrapper, Switch } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components';
import { Unreachable } from '@affine/env/constant'; import { Unreachable } from '@affine/env/constant';
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
@@ -11,7 +11,6 @@ import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-
import type { FC } from 'react'; import type { FC } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useToggleWorkspacePublish } from '../../../hooks/affine/use-toggle-workspace-publish';
import type { AffineOfficialWorkspace } from '../../../shared'; import type { AffineOfficialWorkspace } from '../../../shared';
import { toast } from '../../../utils'; import { toast } from '../../../utils';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal'; import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
@@ -26,13 +25,13 @@ export type PublishPanelLocalProps = WorkspaceSettingDetailProps & {
workspace: LocalWorkspace; workspace: LocalWorkspace;
}; };
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & { export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
workspace: AffineLegacyCloudWorkspace; workspace: AffineCloudWorkspace;
}; };
const PublishPanelAffine: FC<PublishPanelAffineProps> = props => { const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
const { workspace } = props; const { workspace } = props;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const toggleWorkspacePublish = useToggleWorkspacePublish(workspace); // const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
const [origin, setOrigin] = useState(''); const [origin, setOrigin] = useState('');
const shareUrl = origin + '/public-workspace/' + workspace.id; const shareUrl = origin + '/public-workspace/' + workspace.id;
@@ -54,13 +53,14 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
<SettingRow <SettingRow
name={t['Publish']()} name={t['Publish']()}
desc={ desc={
workspace.public ? t['Unpublished hint']() : t['Published hint']() // workspace.public ? t['Unpublished hint']() : t['Published hint']()
'UNFINISHED'
} }
> >
<Switch {/* <Switch
checked={workspace.public} checked={workspace.public}
onChange={checked => toggleWorkspacePublish(checked)} onChange={checked => toggleWorkspacePublish(checked)}
/> /> */}
</SettingRow> </SettingRow>
<FlexWrapper justifyContent="space-between"> <FlexWrapper justifyContent="space-between">
<Button <Button
@@ -150,7 +150,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
onConfirm={() => { onConfirm={() => {
onTransferWorkspace( onTransferWorkspace(
WorkspaceFlavour.LOCAL, WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE, WorkspaceFlavour.AFFINE_CLOUD,
workspace workspace
); );
setOpen(false); setOpen(false);
@@ -169,7 +169,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
}; };
export const PublishPanel: FC<PublishPanelProps> = props => { export const PublishPanel: FC<PublishPanelProps> = props => {
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) { if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return <PublishPanelAffine {...props} workspace={props.workspace} />; return <PublishPanelAffine {...props} workspace={props.workspace} />;
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { } else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <PublishPanelLocal {...props} workspace={props.workspace} />; return <PublishPanelLocal {...props} workspace={props.workspace} />;

View File

@@ -3,7 +3,7 @@ import {
type SettingModalProps, type SettingModalProps,
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
@@ -77,7 +77,7 @@ export const SettingModal: React.FC<SettingModalProps> = ({
generalSettingList={generalSettingList} generalSettingList={generalSettingList}
onGeneralSettingClick={onGeneralSettingClick} onGeneralSettingClick={onGeneralSettingClick}
currentWorkspace={ currentWorkspace={
currentWorkspace as AffineLegacyCloudWorkspace | LocalWorkspace currentWorkspace as AffineCloudWorkspace | LocalWorkspace
} }
workspaceList={workspaceList} workspaceList={workspaceList}
onWorkspaceSettingClick={onWorkspaceSettingClick} onWorkspaceSettingClick={onWorkspaceSettingClick}

View File

@@ -1,7 +1,7 @@
import { UserAvatar } from '@affine/component/user-avatar'; import { UserAvatar } from '@affine/component/user-avatar';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -37,7 +37,7 @@ export const SettingSidebar = ({
currentWorkspace: Workspace; currentWorkspace: Workspace;
workspaceList: Workspace[]; workspaceList: Workspace[];
onWorkspaceSettingClick: ( onWorkspaceSettingClick: (
workspace: AffineLegacyCloudWorkspace | LocalWorkspace workspace: AffineCloudWorkspace | LocalWorkspace
) => void; ) => void;
selectedWorkspace: Workspace | null; selectedWorkspace: Workspace | null;
@@ -118,7 +118,7 @@ const WorkspaceListItem = ({
isCurrent, isCurrent,
isActive, isActive,
}: { }: {
workspace: AffineLegacyCloudWorkspace | LocalWorkspace; workspace: AffineCloudWorkspace | LocalWorkspace;
onClick: () => void; onClick: () => void;
isCurrent: boolean; isCurrent: boolean;
isActive: boolean; isActive: boolean;

View File

@@ -1,6 +1,6 @@
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
export type Workspace = AffineLegacyCloudWorkspace | LocalWorkspace; export type Workspace = AffineCloudWorkspace | LocalWorkspace;

View File

@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons'; import { CloseIcon } from '@blocksuite/icons';
import type React from 'react'; import type React from 'react';
import { useCurrentUser } from '../../../hooks/current/use-current-user';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style'; import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
export type TransformWorkspaceToAffineModalProps = { export type TransformWorkspaceToAffineModalProps = {
@@ -16,7 +15,6 @@ export const TransformWorkspaceToAffineModal: React.FC<
TransformWorkspaceToAffineModalProps TransformWorkspaceToAffineModalProps
> = ({ open, onClose, onConform }) => { > = ({ open, onClose, onConform }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const user = useCurrentUser();
return ( return (
<Modal <Modal
@@ -41,7 +39,7 @@ export const TransformWorkspaceToAffineModal: React.FC<
type="primary" type="primary"
onClick={onConform} onClick={onConform}
> >
{user ? t['Enable']() : t['Sign in and Enable']()} {t['Sign in and Enable']()}
</StyleButton> </StyleButton>
<StyleButton <StyleButton
shape="round" shape="round"

View File

@@ -1,208 +0,0 @@
// import { styled } from '@affine/component';
// import { FlexWrapper } from '@affine/component';
import { globalStyle, style, styleVariants } from '@vanilla-extract/css';
export const container = style({
display: 'flex',
flexDirection: 'column',
marginTop: '52px',
padding: '0 52px 52px 52px',
height: 'calc(100vh - 52px)',
overflow: 'auto',
});
export const sidebar = style({
marginTop: '52px',
});
export const content = style({
flex: 1,
marginTop: '40px',
});
const baseAvatar = style({
position: 'relative',
marginRight: '20px',
cursor: 'pointer',
});
globalStyle(`${baseAvatar} .camera-icon`, {
position: 'absolute',
top: 0,
left: 0,
display: 'none',
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundColor: 'rgba(60, 61, 63, 0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
});
globalStyle(`${baseAvatar}:hover .camera-icon`, {
display: 'flex',
});
export const avatar = styleVariants({
disabled: [
baseAvatar,
{
cursor: 'default',
},
],
enabled: [
baseAvatar,
{
cursor: 'pointer',
},
],
});
const baseTagItem = style({
display: 'flex',
margin: '0 48px 0 0',
height: '34px',
fontWeight: '500',
fontSize: 'var(--affine-font-h6)',
lineHeight: 'var(--affine-line-height)',
cursor: 'pointer',
transition: 'all 0.15s ease',
});
export const tagItem = styleVariants({
active: [
baseTagItem,
{
color: 'var(--affine-primary-color)',
},
],
inactive: [
baseTagItem,
{
color: 'var(--affine-text-secondary-color)',
},
],
});
export const settingKey = style({
width: '140px',
fontSize: 'var(--affine-font-base)',
fontWeight: 500,
marginRight: '56px',
flexShrink: 0,
});
export const settingItemLabel = style({
fontSize: 'var(--affine-font-base)',
fontWeight: 600,
flexShrink: 0,
});
export const settingItemLabelHint = style({
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-secondary-color)',
fontWeight: 400,
flexShrink: 0,
marginTop: '4px',
});
export const settingsCannotDelete = style([
settingItemLabelHint,
{
color: 'var(--affine-warning-color)',
},
]);
export const row = style({
padding: '40px 0',
display: 'flex',
columnGap: '60px',
rowGap: '12px',
selectors: {
'&': {
borderBottom: '1px solid var(--affine-border-color)',
},
'&:first-child': {
paddingTop: 0,
},
},
flexWrap: 'wrap',
});
export const col = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
selectors: {
[`${row} &:nth-child(1)`]: {
flex: '3 0 200px',
},
[`${row} &:nth-child(2)`]: {
flex: '5 0 240px',
},
[`${row} &:nth-child(3)`]: {
flex: '2 0 200px',
alignItems: 'flex-end',
},
},
});
export const workspaceName = style({
fontWeight: '400',
fontSize: 'var(--affine-font-h6)',
});
export const indicator = style({
height: '2px',
background: 'var(--affine-primary-color)',
position: 'absolute',
left: '0',
bottom: '0',
transition: 'left .3s, width .3s',
});
export const tabButtonWrapper = style({
display: 'flex',
position: 'sticky',
top: '0',
background: 'var(--affine-background-primary-color)',
zIndex: 1,
});
export const storageTypeWrapper = style({
width: '100%',
display: 'flex',
alignItems: 'flex-start',
padding: '12px',
borderRadius: '10px',
gap: '12px',
boxShadow: 'var(--affine-shadow-1)',
cursor: 'pointer',
selectors: {
'&:hover': {
boxShadow: 'var(--affine-shadow-2)',
},
'&:not(:last-child)': {
marginBottom: '12px',
},
'&[data-disabled="true"]': {
cursor: 'default',
pointerEvents: 'none',
},
},
});
export const storageTypeLabelWrapper = style({
flex: 1,
});
export const storageTypeLabel = style({
fontSize: 'var(--affine-font-base)',
});
export const storageTypeLabelHint = style({
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-secondary-color)',
});

View File

@@ -1,169 +0,0 @@
import type { SettingPanel, WorkspaceRegistry } from '@affine/env/workspace';
import { settingPanel, WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { MouseEvent } from 'react';
import type React from 'react';
import { Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
import { preload } from 'swr';
import { fetcher, QueryKey } from '../../../adapters/affine/fetcher';
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
import type { AffineOfficialWorkspace } from '../../../shared';
import * as style from './index.css';
import { CollaborationPanel } from './panel/collaboration';
import { ExportPanel } from './panel/export';
import { GeneralPanel } from './panel/general';
import { PublishPanel } from './panel/publish';
import { SyncPanel } from './panel/sync';
export type WorkspaceSettingDetailProps = {
workspace: AffineOfficialWorkspace;
currentTab: SettingPanel;
onChangeTab: (tab: SettingPanel) => void;
onDeleteWorkspace: () => Promise<void>;
onTransferWorkspace: <
From extends WorkspaceFlavour,
To extends WorkspaceFlavour
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
};
export type PanelProps = WorkspaceSettingDetailProps;
type Name = 'General' | 'Sync' | 'Collaboration' | 'Publish' | 'Export';
const panelMap = {
[settingPanel.General]: {
name: 'General',
ui: GeneralPanel,
},
[settingPanel.Sync]: {
name: 'Sync',
enable: (flavour: WorkspaceFlavour) => flavour === WorkspaceFlavour.AFFINE,
ui: SyncPanel,
},
[settingPanel.Collaboration]: {
name: 'Collaboration',
ui: CollaborationPanel,
},
[settingPanel.Publish]: {
name: 'Publish',
ui: PublishPanel,
},
[settingPanel.Export]: {
name: 'Export',
ui: ExportPanel,
},
} satisfies {
[Key in SettingPanel]: {
name: Name;
enable?: (flavour: WorkspaceFlavour) => boolean;
ui: React.FC<PanelProps>;
};
};
function assertInstanceOf<T, U extends T>(
obj: T,
type: new (...args: any[]) => U
): asserts obj is U {
if (!(obj instanceof type)) {
throw new Error('Object is not instance of type');
}
}
export const WorkspaceSettingDetail: React.FC<
WorkspaceSettingDetailProps
> = props => {
const {
workspace,
currentTab,
onChangeTab,
// onDeleteWorkspace,
// onTransferWorkspace,
} = props;
const isAffine = workspace.flavour === 'affine';
const isOwner = useIsWorkspaceOwner(workspace);
if (!(workspace.flavour === 'affine' || workspace.flavour === 'local')) {
throw new Error('Unsupported workspace flavour');
}
if (!(currentTab in panelMap)) {
throw new Error('Invalid activeTab: ' + currentTab);
}
const t = useAFFiNEI18N();
const workspaceId = workspace.id;
useEffect(() => {
if (isAffine && isOwner) {
preload([QueryKey.getMembers, workspaceId], fetcher).catch(console.error);
}
}, [isAffine, isOwner, workspaceId]);
const containerRef = useRef<HTMLDivElement | null>(null);
const indicatorRef = useRef<HTMLDivElement | null>(null);
const startTransaction = useCallback(() => {
if (indicatorRef.current && containerRef.current) {
const indicator = indicatorRef.current;
const activeTabElement = containerRef.current.querySelector(
`[data-tab-key="${currentTab}"]`
);
assertInstanceOf(activeTabElement, HTMLElement);
requestAnimationFrame(() => {
indicator.style.left = `${activeTabElement.offsetLeft}px`;
indicator.style.width = `${activeTabElement.offsetWidth}px`;
});
}
}, [currentTab]);
const handleTabClick = useCallback(
(event: MouseEvent<HTMLElement>) => {
assertInstanceOf(event.target, HTMLElement);
const key = event.target.getAttribute('data-tab-key');
if (!key || !(key in panelMap)) {
throw new Error('data-tab-key is invalid: ' + key);
}
onChangeTab(key as SettingPanel);
startTransaction();
},
[onChangeTab, startTransaction]
);
const Component = useMemo(() => panelMap[currentTab].ui, [currentTab]);
return (
<div
className={style.container}
aria-label="workspace-setting-detail"
ref={containerRef}
>
<div className={style.tabButtonWrapper}>
{Object.entries(panelMap).map(([key, value]) => {
if ('enable' in value && !value.enable(workspace.flavour)) {
return null;
}
return (
<div
className={
style.tagItem[currentTab === key ? 'active' : 'inactive']
}
key={key}
data-tab-key={key}
onClick={handleTabClick}
>
{t[value.name]()}
</div>
);
})}
<div
className={style.indicator}
ref={ref => {
indicatorRef.current = ref;
startTransaction();
}}
/>
</div>
<div className={style.content}>
{/* todo: add skeleton */}
<Suspense fallback="loading panel...">
<Component {...props} key={currentTab} data-tab-ui={currentTab} />
</Suspense>
</div>
</div>
);
};

View File

@@ -1,229 +0,0 @@
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
import { Unreachable } from '@affine/env/constant';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteTemporarilyIcon,
EmailIcon,
MoreVerticalIcon,
} from '@blocksuite/icons';
import type React from 'react';
import { useCallback, useState } from 'react';
import { useMembers } from '../../../../../hooks/affine/use-members';
import { toast } from '../../../../../utils';
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
import type { PanelProps } from '../../index';
import { InviteMemberModal } from './invite-member-modal';
import {
StyledMemberAvatar,
StyledMemberButtonContainer,
StyledMemberContainer,
StyledMemberEmail,
StyledMemberInfo,
StyledMemberListContainer,
StyledMemberListItem,
StyledMemberName,
StyledMemberNameContainer,
StyledMemberRoleContainer,
StyledMemberTitleContainer,
StyledMoreVerticalButton,
StyledMoreVerticalDiv,
} from './style';
const AffineRemoteCollaborationPanel: React.FC<
Omit<PanelProps, 'workspace'> & {
workspace: AffineLegacyCloudWorkspace;
}
> = ({ workspace }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const t = useAFFiNEI18N();
const { members, removeMember } = useMembers(workspace.id);
return (
<>
<StyledMemberContainer>
<ul>
<StyledMemberTitleContainer>
<StyledMemberNameContainer>
{t['Users']()} (
<span data-testid="member-length">{members.length}</span>)
</StyledMemberNameContainer>
<StyledMemberRoleContainer>
{t['Access level']()}
</StyledMemberRoleContainer>
<div style={{ width: '24px', paddingRight: '48px' }}></div>
</StyledMemberTitleContainer>
</ul>
<StyledMemberListContainer>
{members.length > 0 && (
<>
{members
.sort((b, a) => a.type - b.type)
.map((member, index) => {
const user = {
avatar_url: '',
id: '',
name: '',
...member.user,
};
return (
<StyledMemberListItem key={index}>
<StyledMemberNameContainer>
<StyledMemberAvatar
alt="member avatar"
src={user.avatar_url}
>
<EmailIcon />
</StyledMemberAvatar>
<StyledMemberInfo>
<StyledMemberName>{user.name}</StyledMemberName>
<StyledMemberEmail>
{member.user.email}
</StyledMemberEmail>
</StyledMemberInfo>
</StyledMemberNameContainer>
<StyledMemberRoleContainer>
{member.accepted
? member.type !== PermissionType.Owner
? t['Member']()
: t['Owner']()
: t['Pending']()}
</StyledMemberRoleContainer>
{member.type === PermissionType.Owner ? (
<StyledMoreVerticalDiv />
) : (
<StyledMoreVerticalButton>
<Menu
content={
<>
<MenuItem
onClick={async () => {
// FIXME: remove ignore
await removeMember(Number(member.id));
toast(
t['Member has been removed']({
name: user.name,
})
);
}}
icon={<DeleteTemporarilyIcon />}
>
{t['Remove from workspace']()}
</MenuItem>
</>
}
placement="bottom"
disablePortal={true}
trigger="click"
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
</StyledMoreVerticalButton>
)}
</StyledMemberListItem>
);
})}
</>
)}
</StyledMemberListContainer>
<StyledMemberButtonContainer>
<Button
onClick={() => {
setIsInviteModalShow(true);
}}
type="primary"
data-testid="invite-members"
shape="circle"
>
{t['Invite Members']()}
</Button>
</StyledMemberButtonContainer>
</StyledMemberContainer>
<InviteMemberModal
onClose={useCallback(() => {
setIsInviteModalShow(false);
}, [])}
onInviteSuccess={useCallback(() => {
setIsInviteModalShow(false);
}, [])}
workspaceId={workspace.id}
open={isInviteModalShow}
/>
</>
);
};
const LocalCollaborationPanel: React.FC<
Omit<PanelProps, 'workspace'> & {
workspace: LocalWorkspace;
}
> = ({ workspace, onTransferWorkspace }) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
return (
<>
<Wrapper marginBottom="42px">{t['Collaboration Description']()}</Wrapper>
<Button
data-testid="local-workspace-enable-cloud-button"
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t['Enable AFFiNE Cloud']()}
</Button>
{runtimeConfig.enableLegacyCloud ? (
<TransformWorkspaceToAffineModal
open={open}
onClose={() => {
setOpen(false);
}}
onConform={() => {
onTransferWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
workspace
);
setOpen(false);
}}
/>
) : (
<TmpDisableAffineCloudModal
open={open}
onClose={() => {
setOpen(false);
}}
/>
)}
</>
);
};
export const CollaborationPanel: React.FC<PanelProps> = props => {
switch (props.workspace.flavour) {
case WorkspaceFlavour.AFFINE: {
const workspace = props.workspace as AffineLegacyCloudWorkspace;
return (
<AffineRemoteCollaborationPanel {...props} workspace={workspace} />
);
}
case WorkspaceFlavour.LOCAL: {
const workspace = props.workspace as LocalWorkspace;
return <LocalCollaborationPanel {...props} workspace={workspace} />;
}
}
throw new Unreachable();
};

View File

@@ -1,218 +0,0 @@
import {
Button,
Input,
Modal,
ModalCloseButton,
ModalWrapper,
MuiAvatar,
styled,
} from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EmailIcon } from '@blocksuite/icons';
import type React from 'react';
import { Suspense, useCallback, useState } from 'react';
import { useMembers } from '../../../../../../hooks/affine/use-members';
import { useUsersByEmail } from '../../../../../../hooks/affine/use-users-by-email';
interface LoginModalProps {
open: boolean;
onClose: () => void;
workspaceId: string;
onInviteSuccess: () => void;
}
const gmailReg =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@(gmail|example)\.(com|org)$/;
const Result: React.FC<{
workspaceId: string;
queryEmail: string;
}> = ({ workspaceId, queryEmail }) => {
const users = useUsersByEmail(workspaceId, queryEmail);
const firstUser = users?.at(0) ?? null;
if (!firstUser || !firstUser.email) {
return null;
}
return (
<Members>
<Member>
{firstUser.avatar_url ? (
<MuiAvatar src={firstUser.avatar_url}></MuiAvatar>
) : (
<MemberIcon>
<EmailIcon></EmailIcon>
</MemberIcon>
)}
<Email>{firstUser.email}</Email>
{/* <div>invited</div> */}
</Member>
</Members>
);
};
export const InviteMemberModal = ({
open,
onClose,
onInviteSuccess,
workspaceId,
}: LoginModalProps) => {
const { inviteMember } = useMembers(workspaceId);
const [email, setEmail] = useState<string>('');
const [showMemberPreview, setShowMemberPreview] = useState(false);
const t = useAFFiNEI18N();
const inputChange = useCallback((value: string) => {
setEmail(value);
}, []);
return (
<div>
<Modal open={open} onClose={onClose}>
<ModalWrapper width={460} height={236}>
<Header>
<ModalCloseButton
onClick={() => {
onClose();
setEmail('');
}}
/>
</Header>
<Content>
<ContentTitle>{t['Invite Members']()}</ContentTitle>
<InviteBox>
<Input
data-testid="invite-member-input"
width={360}
value={email}
onChange={inputChange}
onFocus={useCallback(() => {
setShowMemberPreview(true);
}, [])}
onBlur={useCallback(() => {
setShowMemberPreview(false);
}, [])}
placeholder={t['Invite placeholder']()}
/>
{showMemberPreview && gmailReg.test(email) && (
<Suspense fallback="loading...">
<Result workspaceId={workspaceId} queryEmail={email} />
</Suspense>
)}
</InviteBox>
</Content>
<Footer>
<Button
data-testid="invite-member-button"
disabled={!gmailReg.test(email)}
shape="circle"
type="primary"
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
onClick={async () => {
await inviteMember(email);
setEmail('');
onInviteSuccess();
}}
>
{t['Invite']()}
</Button>
</Footer>
</ModalWrapper>
</Modal>
</div>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const Footer = styled('div')({
height: '102px',
margin: '32px 0',
textAlign: 'center',
});
const InviteBox = styled('div')({
position: 'relative',
});
const Members = styled('div')(() => {
return {
position: 'absolute',
width: '100%',
background: 'var(--affine-background-primary-color)',
textAlign: 'left',
zIndex: 1,
borderRadius: '0px 10px 10px 10px',
height: '56px',
padding: '8px 12px',
input: {
'&::placeholder': {
color: 'var(--affine-placeholder-color)',
},
},
};
});
// const NoFind = styled('div')(({ theme }) => {
// return {
// color: 'var(--affine-icon-color)',
// fontSize: 'var(--affine-font-sm)',
// lineHeight: '40px',
// userSelect: 'none',
// width: '100%',
// };
// });
const Member = styled('div')(() => {
return {
color: 'var(--affine-icon-color)',
fontSize: 'var(--affine-font-sm)',
lineHeight: '40px',
userSelect: 'none',
display: 'flex',
};
});
const MemberIcon = styled('div')(() => {
return {
width: '40px',
height: '40px',
borderRadius: '50%',
color: 'var(--affine-primary-color)',
background: '#F5F5F5',
textAlign: 'center',
lineHeight: '45px',
// icon size
fontSize: '20px',
overflow: 'hidden',
img: {
width: '100%',
height: '100%',
},
};
});
const Email = styled('div')(() => {
return {
flex: '1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginLeft: '8px',
};
});

View File

@@ -1,101 +0,0 @@
import { styled } from '@affine/component';
import { MuiAvatar } from '@affine/component';
export const StyledMemberTitleContainer = styled('li')(() => {
return {
display: 'flex',
fontWeight: '500',
marginBottom: '42px',
flex: 1,
};
});
export const StyledMemberContainer = styled('div')(() => {
return {
display: 'flex',
height: '100%',
flexDirection: 'column',
overflow: 'hidden',
};
});
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
return { height: '40px', width: '40px' };
});
export const StyledMemberNameContainer = styled('div')(() => {
return {
display: 'flex',
alignItems: 'center',
flex: '2 0 402px',
};
});
export const StyledMemberRoleContainer = styled('div')(() => {
return {
display: 'flex',
alignItems: 'center',
flex: '1 0 222px',
};
});
export const StyledMemberListContainer = styled('ul')(() => {
return {
overflowY: 'scroll',
flexGrow: 1,
paddingBottom: '58px',
};
});
export const StyledMemberListItem = styled('li')(() => {
return {
display: 'flex',
alignItems: 'center',
height: '72px',
width: '100%',
};
});
export const StyledMemberInfo = styled('div')(() => {
return {
paddingLeft: '12px',
};
});
export const StyledMemberName = styled('div')(() => {
return {
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
color: 'var(--affine-text-primary-color)',
};
});
export const StyledMemberEmail = styled('div')(() => {
return {
fontWeight: '400',
fontSize: '16px',
lineHeight: '22px',
color: 'var(--affine-icon-color)',
};
});
export const StyledMemberButtonContainer = styled('div')(() => {
return {
position: 'fixed',
bottom: '20px',
};
});
export const StyledMoreVerticalDiv = styled('div')(() => {
return {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '24px',
height: '24px',
cursor: 'pointer',
paddingRight: '48px',
};
});
export const StyledMoreVerticalButton = styled(StyledMoreVerticalDiv)``;

View File

@@ -1,35 +0,0 @@
import { Button, toast, Wrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import { useAtomValue } from 'jotai';
export const ExportPanel = () => {
const id = useAtomValue(rootCurrentWorkspaceIdAtom);
const t = useAFFiNEI18N();
return (
<>
<Wrapper marginBottom="42px"> {t['Export Description']()}</Wrapper>
<Button
type="light"
shape="circle"
disabled={
!environment.isDesktop || !id || !runtimeConfig.enableSQLiteProvider
}
data-testid="export-affine-backup"
onClick={async () => {
if (id) {
const result = await window.apis?.dialog.saveDBFileAs(id);
if (result?.error) {
// @ts-expect-error: result.error is dynamic
toast(t[result.error]());
} else if (!result?.canceled) {
toast(t['Export success']());
}
}
}}
>
{t['Export AFFiNE backup file']()}
</Button>
</>
);
};

View File

@@ -1,112 +0,0 @@
import { Button, Input, Modal, ModalCloseButton } from '@affine/component';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useCallback, useState } from 'react';
import type { AffineOfficialWorkspace } from '../../../../../../shared';
import { toast } from '../../../../../../utils';
import {
StyledButtonContent,
StyledInputContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
StyledWorkspaceName,
} from './style';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
workspace: AffineOfficialWorkspace;
onDeleteWorkspace: () => Promise<void>;
}
export const WorkspaceDeleteModal = ({
open,
onClose,
workspace,
onDeleteWorkspace,
}: WorkspaceDeleteProps) => {
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace ?? null
);
const [deleteStr, setDeleteStr] = useState<string>('');
const allowDelete = deleteStr === workspaceName;
const t = useAFFiNEI18N();
const handleDelete = useCallback(() => {
onDeleteWorkspace()
.then(() => {
toast(t['Successfully deleted'](), {
portal: document.body,
});
})
.catch(() => {
// ignore error
});
}, [onDeleteWorkspace, t]);
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t['Delete Workspace']()}?</StyledModalHeader>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspaceName } as any}
</StyledWorkspaceName>
) cannot be undone, please proceed with caution. All contents will
be lost.
</Trans>
</StyledTextContent>
) : (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description2">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspaceName } as any}
</StyledWorkspaceName>
) will delete both local and cloud data, this operation cannot be
undone, please proceed with caution.
</Trans>
</StyledTextContent>
)}
<StyledInputContent>
<Input
ref={ref => {
if (ref) {
setTimeout(() => ref.focus(), 0);
}
}}
onChange={setDeleteStr}
data-testid="delete-workspace-input"
placeholder={t['Placeholder of delete workspace']()}
value={deleteStr}
width={315}
height={42}
/>
</StyledInputContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t['Cancel']()}
</Button>
<Button
data-testid="delete-workspace-confirm-button"
disabled={!allowDelete}
onClick={handleDelete}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t['Delete']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};

View File

@@ -1,76 +0,0 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(() => {
return {
position: 'relative',
padding: '0px',
width: '560px',
background: 'var(--affine-white)',
borderRadius: '12px',
// height: '312px',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
margin: '44px 0px 12px 0px',
width: '560px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'left',
};
});
export const StyledInputContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '24px 0',
fontSize: 'var(--affine-font-base)',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
marginBottom: '42px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
});
export const StyledWorkspaceName = styled('span')(() => {
return {
fontWeight: '600',
};
});
// export const StyledCancelButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });

View File

@@ -1,18 +0,0 @@
export const CameraIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.6236 4.25001C10.635 4.25001 10.6467 4.25002 10.6584 4.25002H13.3416C13.3533 4.25002 13.365 4.25001 13.3764 4.25001C13.5609 4.24995 13.7105 4.2499 13.8543 4.26611C14.5981 4.34997 15.2693 4.75627 15.6826 5.38026C15.7624 5.50084 15.83 5.63398 15.9121 5.79586C15.9173 5.80613 15.9226 5.81652 15.9279 5.82703C15.9538 5.87792 15.9679 5.90562 15.9789 5.9261C15.9832 5.9341 15.9857 5.93861 15.9869 5.94065C16.0076 5.97069 16.0435 5.99406 16.0878 5.99905L16.0849 5.99877C16.0849 5.99877 16.0907 5.99918 16.1047 5.99947C16.1286 5.99998 16.1604 6.00002 16.2181 6.00002L17.185 6.00001C17.6577 6 18.0566 5.99999 18.3833 6.02627C18.7252 6.05377 19.0531 6.11364 19.3656 6.27035C19.8402 6.50842 20.2283 6.88944 20.4723 7.36077C20.6336 7.67233 20.6951 7.99944 20.7232 8.33858C20.75 8.66166 20.75 9.05554 20.75 9.51992V16.2301C20.75 16.6945 20.75 17.0884 20.7232 17.4114C20.6951 17.7506 20.6336 18.0777 20.4723 18.3893C20.2283 18.8606 19.8402 19.2416 19.3656 19.4797C19.0531 19.6364 18.7252 19.6963 18.3833 19.7238C18.0566 19.75 17.6578 19.75 17.185 19.75H6.81497C6.34225 19.75 5.9434 19.75 5.61668 19.7238C5.27477 19.6963 4.94688 19.6364 4.63444 19.4797C4.15978 19.2416 3.77167 18.8606 3.52771 18.3893C3.36644 18.0777 3.30494 17.7506 3.27679 17.4114C3.24998 17.0884 3.24999 16.6945 3.25 16.2302V9.51987C3.24999 9.05551 3.24998 8.66164 3.27679 8.33858C3.30494 7.99944 3.36644 7.67233 3.52771 7.36077C3.77167 6.88944 4.15978 6.50842 4.63444 6.27035C4.94688 6.11364 5.27477 6.05377 5.61668 6.02627C5.9434 5.99999 6.34225 6 6.81498 6.00001L7.78191 6.00002C7.83959 6.00002 7.87142 5.99998 7.8953 5.99947C7.90607 5.99924 7.91176 5.99897 7.91398 5.99884C7.95747 5.99343 7.99267 5.9703 8.01312 5.94066C8.01429 5.93863 8.01684 5.93412 8.02113 5.9261C8.0321 5.90561 8.04622 5.87791 8.07206 5.82703C8.07739 5.81653 8.08266 5.80615 8.08787 5.79588C8.17004 5.63397 8.23759 5.50086 8.31745 5.38026C8.73067 4.75627 9.40192 4.34997 10.1457 4.26611C10.2895 4.2499 10.4391 4.24995 10.6236 4.25001ZM10.6584 5.75002C10.422 5.75002 10.3627 5.75114 10.3138 5.75666C10.0055 5.79142 9.73316 5.95919 9.56809 6.20845C9.54218 6.24758 9.51544 6.29761 9.40943 6.50633C9.40611 6.51287 9.40274 6.5195 9.39934 6.52622C9.36115 6.60161 9.31758 6.68761 9.26505 6.76694C8.9964 7.17261 8.56105 7.4354 8.08026 7.48961C7.98625 7.50021 7.89021 7.50011 7.80434 7.50003C7.79678 7.50002 7.7893 7.50002 7.78191 7.50002H6.84445C6.33444 7.50002 5.99634 7.50058 5.73693 7.52144C5.48594 7.54163 5.37478 7.57713 5.30693 7.61115C5.11257 7.70864 4.95675 7.86306 4.85983 8.05029C4.82733 8.11308 4.79194 8.21816 4.77165 8.46266C4.7506 8.71626 4.75 9.0474 4.75 9.55001V16.2C4.75 16.7026 4.7506 17.0338 4.77165 17.2874C4.79194 17.5319 4.82733 17.6369 4.85983 17.6997C4.95675 17.887 5.11257 18.0414 5.30693 18.1389C5.37478 18.1729 5.48594 18.2084 5.73693 18.2286C5.99634 18.2494 6.33444 18.25 6.84445 18.25H17.1556C17.6656 18.25 18.0037 18.2494 18.2631 18.2286C18.5141 18.2084 18.6252 18.1729 18.6931 18.1389C18.8874 18.0414 19.0433 17.887 19.1402 17.6997C19.1727 17.6369 19.2081 17.5319 19.2283 17.2874C19.2494 17.0338 19.25 16.7026 19.25 16.2V9.55001C19.25 9.0474 19.2494 8.71626 19.2283 8.46266C19.2081 8.21816 19.1727 8.11308 19.1402 8.05029C19.0433 7.86306 18.8874 7.70864 18.6931 7.61115C18.6252 7.57713 18.5141 7.54163 18.2631 7.52144C18.0037 7.50058 17.6656 7.50002 17.1556 7.50002H16.2181C16.2107 7.50002 16.2032 7.50002 16.1957 7.50003C16.1098 7.50011 16.0138 7.50021 15.9197 7.48961C15.4389 7.4354 15.0036 7.17261 14.735 6.76694C14.6824 6.68761 14.6389 6.60163 14.6007 6.52622C14.5973 6.5195 14.5939 6.51287 14.5906 6.50633C14.4846 6.29763 14.4578 6.24758 14.4319 6.20846C14.2668 5.95919 13.9945 5.79142 13.6862 5.75666C13.6373 5.75114 13.578 5.75002 13.3416 5.75002H10.6584ZM12 11C10.9303 11 10.0833 11.8506 10.0833 12.875C10.0833 13.8995 10.9303 14.75 12 14.75C13.0697 14.75 13.9167 13.8995 13.9167 12.875C13.9167 11.8506 13.0697 11 12 11ZM8.58333 12.875C8.58333 11 10.1242 9.50002 12 9.50002C13.8758 9.50002 15.4167 11 15.4167 12.875C15.4167 14.7501 13.8758 16.25 12 16.25C10.1242 16.25 8.58333 14.7501 8.58333 12.875Z"
fill="white"
/>
</svg>
);
};

View File

@@ -1,283 +0,0 @@
import { Button, toast } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ArrowRightSmallIcon,
DeleteIcon,
FolderIcon,
MoveToIcon,
SaveIcon,
} from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import type React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
import { Upload } from '../../../../pure/file-upload';
import type { PanelProps } from '../../index';
import * as style from '../../index.css';
import { WorkspaceDeleteModal } from './delete';
import { CameraIcon } from './icons';
import { WorkspaceLeave } from './leave';
import { StyledInput } from './style';
const useShowOpenDBFile = (workspaceId: string) => {
const [show, setShow] = useState(false);
useEffect(() => {
if (
window.apis &&
window.events &&
environment.isDesktop &&
runtimeConfig.enableSQLiteProvider
) {
window.apis.workspace
.getMeta(workspaceId)
.then(meta => {
setShow(!!meta.secondaryDBPath);
})
.catch(err => {
console.error(err);
});
return window.events.workspace.onMetaChange((newMeta: any) => {
if (newMeta.workspaceId === workspaceId) {
const meta = newMeta.meta;
setShow(!!meta.secondaryDBPath);
}
});
}
}, [workspaceId]);
return show;
};
export const GeneralPanel: React.FC<PanelProps> = ({
workspace,
onDeleteWorkspace,
}) => {
const [showDelete, setShowDelete] = useState<boolean>(false);
const [showLeave, setShowLeave] = useState<boolean>(false);
const [name, setName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace
);
const [input, setInput] = useState<string>(name);
const isOwner = useIsWorkspaceOwner(workspace);
const t = useAFFiNEI18N();
const handleUpdateWorkspaceName = (name: string) => {
setName(name);
toast(t['Update workspace name success']());
};
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace
);
return (
<>
<div data-testid="avatar-row" className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>
{t['Workspace Avatar']()}
</div>
<div className={style.settingItemLabelHint}>
{t['Change avatar hint']()}
</div>
</div>
<div className={clsx(style.col)}>
<div className={style.avatar[isOwner ? 'enabled' : 'disabled']}>
{isOwner ? (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={update}
data-testid="upload-avatar"
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceAvatar size={72} workspace={workspace} />
</>
</Upload>
) : (
<WorkspaceAvatar size={72} workspace={workspace} />
)}
</div>
</div>
<div className={clsx(style.col)}></div>
</div>
<div data-testid="workspace-name-row" className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>{t['Workspace Name']()}</div>
<div className={style.settingItemLabelHint}>
{t['Change workspace name hint']()}
</div>
</div>
<div className={style.col}>
<StyledInput
height={38}
value={input}
data-testid="workspace-name-input"
placeholder={t['Workspace Name']()}
maxLength={64}
minLength={0}
onChange={setInput}
></StyledInput>
</div>
<div className={style.col}>
<Button
type="light"
size="middle"
data-testid="save-workspace-name"
icon={<SaveIcon />}
disabled={input === workspace.blockSuiteWorkspace.meta.name}
onClick={() => {
handleUpdateWorkspaceName(input);
}}
>
{t['Save']()}
</Button>
</div>
</div>
{environment.isDesktop && runtimeConfig.enableSQLiteProvider ? (
<DesktopClientOnly workspaceId={workspace.id} />
) : null}
<div className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>
{t['Delete Workspace']()}
</div>
<div className={style.settingItemLabelHint}>
{t['Delete Workspace Label Hint']()}
</div>
</div>
<div className={style.col}></div>
<div className={style.col}>
{isOwner ? (
<>
<Button
type="warning"
data-testid="delete-workspace-button"
size="middle"
icon={<DeleteIcon />}
onClick={() => {
setShowDelete(true);
}}
>
{t['Delete']()}
</Button>
<WorkspaceDeleteModal
onDeleteWorkspace={onDeleteWorkspace}
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
workspace={workspace}
/>
</>
) : (
<>
<Button
type="warning"
size="middle"
onClick={() => {
setShowLeave(true);
}}
>
{t['Leave']()}
</Button>
<WorkspaceLeave
open={showLeave}
onClose={() => {
setShowLeave(false);
}}
/>
</>
)}
</div>
</div>
</>
);
};
function DesktopClientOnly({ workspaceId }: { workspaceId: string }) {
const t = useAFFiNEI18N();
const showOpenFolder = useShowOpenDBFile(workspaceId);
const onRevealDBFile = useCallback(() => {
if (environment.isDesktop && runtimeConfig.enableSQLiteProvider) {
window.apis?.dialog.revealDBFile(workspaceId).catch(err => {
console.error(err);
});
}
}, [workspaceId]);
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
const handleMoveTo = useCallback(() => {
if (moveToInProgress) {
return;
}
setMoveToInProgress(true);
window.apis?.dialog
.moveDBFile(workspaceId)
.then(result => {
if (!result?.error && !result?.canceled) {
toast(t['Move folder success']());
} else if (result?.error) {
// @ts-expect-error: result.error is dynamic
toast(t[result.error]());
}
})
.catch(() => {
toast(t['UNKNOWN_ERROR']());
})
.finally(() => {
setMoveToInProgress(false);
});
}, [moveToInProgress, t, workspaceId]);
const openFolderNode = showOpenFolder ? (
<div className={style.storageTypeWrapper} onClick={onRevealDBFile}>
<FolderIcon color="var(--affine-primary-color)" />
<div className={style.storageTypeLabelWrapper}>
<div className={style.storageTypeLabel}>{t['Open folder']()}</div>
<div className={style.storageTypeLabelHint}>
{t['Open folder hint']()}
</div>
</div>
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
</div>
) : null;
return (
<div className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>{t['Storage Folder']()}</div>
<div className={style.settingItemLabelHint}>
{t['Storage Folder Hint']()}
</div>
</div>
<div className={style.col}>
{openFolderNode}
<div
data-testid="move-folder"
data-disabled={moveToInProgress}
className={style.storageTypeWrapper}
onClick={handleMoveTo}
>
<MoveToIcon color="var(--affine-primary-color)" />
<div className={style.storageTypeLabelWrapper}>
<div className={style.storageTypeLabel}>{t['Move folder']()}</div>
<div className={style.storageTypeLabelHint}>
{t['Move folder hint']()}
</div>
</div>
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
</div>
</div>
<div className={style.col}></div>
</div>
);
}

View File

@@ -1,50 +0,0 @@
import { Modal } from '@affine/component';
import { ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
StyledButtonContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
} from './style';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
}
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
// const { leaveWorkSpace } = useWorkspaceHelper();
const t = useAFFiNEI18N();
const handleLeave = async () => {
// await leaveWorkSpace();
onClose();
};
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t['Leave Workspace']()}</StyledModalHeader>
<StyledTextContent>
{t['Leave Workspace Description']()}
</StyledTextContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t['Cancel']()}
</Button>
<Button
onClick={handleLeave}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t['Leave']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};

View File

@@ -1,45 +0,0 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(() => {
return {
position: 'relative',
padding: '0px',
width: '460px',
background: 'var(--affine-white)',
borderRadius: '12px',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
margin: '44px 0px 12px 0px',
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0px 0 32px 0',
};
});

View File

@@ -1,51 +0,0 @@
import { displayFlex, styled } from '@affine/component';
import { Input } from '@affine/component';
export const StyledInput = Input;
export const StyledWorkspaceInfo = styled('div')(() => {
return {
...displayFlex('flex-start', 'center'),
fontSize: '20px',
span: {
fontSize: 'var(--affine-font-base)',
marginLeft: '15px',
},
};
});
export const StyledAvatar = styled('div')(
({ disabled }: { disabled: boolean }) => {
return {
position: 'relative',
marginRight: '20px',
cursor: disabled ? 'default' : 'pointer',
':hover': {
'.camera-icon': {
display: 'flex',
},
},
'.camera-icon': {
position: 'absolute',
top: 0,
left: 0,
display: 'none',
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundColor: 'rgba(60, 61, 63, 0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
};
}
);
export const StyledEditButton = styled('div')(() => {
return {
color: 'var(--affine-primary-color)',
cursor: 'pointer',
marginLeft: '36px',
};
});

View File

@@ -1,171 +0,0 @@
import {
Button,
Content,
FlexWrapper,
Input,
Wrapper,
} from '@affine/component';
import { isBrowser, Unreachable } from '@affine/env/constant';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Box } from '@mui/material';
import type React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish';
import type { AffineOfficialWorkspace } from '../../../../../shared';
import { toast } from '../../../../../utils';
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
import type { WorkspaceSettingDetailProps } from '../../index';
export type PublishPanelProps = WorkspaceSettingDetailProps & {
workspace: AffineOfficialWorkspace;
};
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
workspace: AffineLegacyCloudWorkspace;
};
const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
workspace,
}) => {
const [origin, setOrigin] = useState('');
useEffect(() => {
setOrigin(
isBrowser && window.location.origin ? window.location.origin : ''
);
}, []);
const shareUrl = origin + '/public-workspace/' + workspace.id;
const t = useAFFiNEI18N();
const publishWorkspace = useToggleWorkspacePublish(workspace);
const copyUrl = useCallback(async () => {
await navigator.clipboard.writeText(shareUrl);
toast(t['Copied link to clipboard']());
}, [shareUrl, t]);
if (workspace.public) {
return (
<>
<Wrapper marginBottom="42px">{t['Published Description']()}</Wrapper>
<Wrapper marginBottom="12px">
<Content weight="500">{t['Share with link']()}</Content>
</Wrapper>
<FlexWrapper>
<Input
data-testid="share-url"
width={582}
value={shareUrl}
disabled={true}
></Input>
<Button
onClick={copyUrl}
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t['Copy Link']()}
</Button>
</FlexWrapper>
<Button
onClick={async () => {
await publishWorkspace(false);
}}
loading={false}
type="danger"
shape="circle"
style={{ marginTop: '38px' }}
>
{t['Stop publishing']()}
</Button>
</>
);
}
return (
<>
<Wrapper marginBottom="42px">{t['Publishing Description']()}</Wrapper>
<Button
data-testid="publish-to-web-button"
onClick={async () => {
await publishWorkspace(true);
}}
type="light"
shape="circle"
>
{t['Publish to web']()}
</Button>
</>
);
};
export type PublishPanelLocalProps = WorkspaceSettingDetailProps & {
workspace: LocalWorkspace;
};
const PublishPanelLocal: React.FC<PublishPanelLocalProps> = ({
workspace,
onTransferWorkspace,
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
return (
<>
<Box
sx={{
marginBottom: '42px',
}}
>
{t['Publishing']()}
</Box>
{/* TmpDisableAffineCloudModal */}
<Button
data-testid="publish-enable-affine-cloud-button"
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t['Enable AFFiNE Cloud']()}
</Button>
{runtimeConfig.enableLegacyCloud ? (
<EnableAffineCloudModal
open={open}
onClose={() => {
setOpen(false);
}}
onConfirm={() => {
onTransferWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
workspace
);
setOpen(false);
}}
/>
) : (
<TmpDisableAffineCloudModal
open={open}
onClose={() => {
setOpen(false);
}}
/>
)}
</>
);
};
export const PublishPanel: React.FC<PublishPanelProps> = props => {
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
return <PublishPanelAffine {...props} workspace={props.workspace} />;
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <PublishPanelLocal {...props} workspace={props.workspace} />;
}
throw new Unreachable();
};

View File

@@ -1,51 +0,0 @@
import { Content, FlexWrapper, styled } from '@affine/component';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import type React from 'react';
import { useCurrentUser } from '../../../../../hooks/current/use-current-user';
import { WorkspaceAvatar } from '../../../../pure/footer';
import type { PanelProps } from '../../index';
export const StyledWorkspaceName = styled('span')(() => {
return {
fontWeight: '400',
fontSize: 'var(--affine-font-h6)',
};
});
export const SyncPanel: React.FC<PanelProps> = ({ workspace }) => {
if (workspace.flavour !== WorkspaceFlavour.AFFINE) {
throw new TypeError('SyncPanel can only be used with Affine workspace');
}
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace
);
const user = useCurrentUser();
const t = useAFFiNEI18N();
return (
<>
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
<WorkspaceAvatar
size={32}
name={name}
avatar={avatar}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{name}</StyledWorkspaceName>
&nbsp;
<Content weight={500}>{t['is a Cloud Workspace']()}</Content>
</FlexWrapper>
<Trans i18nKey="Cloud Workspace Description">
All data will be synchronised and saved to the AFFiNE account
{{
email: user?.email,
}}
</Trans>
</>
);
};

View File

@@ -1,7 +1,7 @@
import { ShareMenu } from '@affine/component/share-menu'; import { ShareMenu } from '@affine/component/share-menu';
import { Unreachable } from '@affine/env/constant'; import { Unreachable } from '@affine/env/constant';
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
@@ -11,21 +11,20 @@ import { useRouter } from 'next/router';
import type React from 'react'; import type React from 'react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useToggleWorkspacePublish } from '../../../../hooks/affine/use-toggle-workspace-publish';
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace'; import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
import { useRouterHelper } from '../../../../hooks/use-router-helper'; import { useRouterHelper } from '../../../../hooks/use-router-helper';
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal'; import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
import type { BaseHeaderProps } from '../header'; import type { BaseHeaderProps } from '../header';
const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => { const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
// todo: these hooks should be moved to the top level // fixme: cloud regression
const togglePublish = useToggleWorkspacePublish( // const togglePublish = useToggleWorkspacePublish(
props.workspace as AffineLegacyCloudWorkspace // props.workspace as AffineCloudWorkspace
); // );
const helper = useRouterHelper(useRouter()); const helper = useRouterHelper(useRouter());
return ( return (
<ShareMenu <ShareMenu
workspace={props.workspace as AffineLegacyCloudWorkspace} workspace={props.workspace as AffineCloudWorkspace}
currentPage={props.currentPage as Page} currentPage={props.currentPage as Page}
onEnableAffineCloud={useCallback(async () => { onEnableAffineCloud={useCallback(async () => {
throw new Unreachable( throw new Unreachable(
@@ -42,12 +41,12 @@ const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
page.workspace.setPageMeta(page.id, { isPublic }); page.workspace.setPageMeta(page.id, { isPublic });
}, [])} }, [])}
toggleWorkspacePublish={useCallback( toggleWorkspacePublish={useCallback(
async (workspace, publish) => { async workspace => {
assertEquals(workspace.flavour, WorkspaceFlavour.AFFINE); assertEquals(workspace.flavour, WorkspaceFlavour.AFFINE_CLOUD);
assertEquals(workspace.id, props.workspace.id); assertEquals(workspace.id, props.workspace.id);
await togglePublish(publish); throw new Error('unreachable');
}, },
[props.workspace.id, togglePublish] [props.workspace.id]
)} )}
/> />
); );
@@ -98,7 +97,7 @@ const LocalHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
onConform={async () => { onConform={async () => {
await onTransformWorkspace( await onTransformWorkspace(
WorkspaceFlavour.LOCAL, WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE, WorkspaceFlavour.AFFINE_CLOUD,
props.workspace as LocalWorkspace props.workspace as LocalWorkspace
); );
setOpen(false); setOpen(false);
@@ -112,7 +111,7 @@ export const HeaderShareMenu: React.FC<BaseHeaderProps> = props => {
if (!runtimeConfig.enableLegacyCloud) { if (!runtimeConfig.enableLegacyCloud) {
return null; return null;
} }
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) { if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return <AffineHeaderShareMenu {...props} />; return <AffineHeaderShareMenu {...props} />;
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { } else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <LocalHeaderShareMenu {...props} />; return <LocalHeaderShareMenu {...props} />;

View File

@@ -1,163 +0,0 @@
import { displayFlex, IconButton, styled, Tooltip } from '@affine/component';
import type { LocalWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
getLoginStorage,
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import { affineAuth } from '@affine/workspace/affine/shared';
import {
CloudWorkspaceIcon,
LocalWorkspaceIcon,
NoNetworkIcon,
} from '@blocksuite/icons';
import { assertEquals, assertExists } from '@blocksuite/store';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
import type { AffineOfficialWorkspace } from '../../../../shared';
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
const IconWrapper = styled('div')(() => {
return {
width: '32px',
height: '32px',
marginRight: '12px',
fontSize: '24px',
color: 'var(--affine-icon-color)',
WebkitAppRegion: 'no-drag',
...displayFlex('center', 'center'),
};
});
const getStatus = (workspace: AffineOfficialWorkspace) => {
if (!navigator.onLine) {
return 'offline';
}
if (workspace.flavour === 'local') {
return 'local';
}
return 'cloud';
};
export const SyncUser = () => {
//#region fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
assertExists(workspace);
const router = useRouter();
const [status, setStatus] = useState<'offline' | 'local' | 'cloud'>(
getStatus(workspace)
);
const [prevWorkspace, setPrevWorkspace] = useState(workspace);
if (prevWorkspace !== workspace) {
setPrevWorkspace(workspace);
setStatus(getStatus(workspace));
}
useEffect(() => {
const online = () => {
setStatus(getStatus(workspace));
};
const offline = () => {
setStatus('offline');
};
window.addEventListener('online', online);
window.addEventListener('offline', offline);
return () => {
window.removeEventListener('online', online);
window.removeEventListener('offline', offline);
};
}, [workspace]);
//#endregion
const [open, setOpen] = useState(false);
const t = useAFFiNEI18N();
const transformWorkspace = useTransformWorkspace();
if (!runtimeConfig.enableLegacyCloud) {
return null;
}
if (status === 'offline') {
return (
<Tooltip
content={t['Please make sure you are online']()}
placement="bottom-end"
>
<IconWrapper>
<NoNetworkIcon />
</IconWrapper>
</Tooltip>
);
}
if (status === 'local') {
return (
<>
<Tooltip
content={t['Saved then enable AFFiNE Cloud']()}
placement="bottom-end"
>
<IconButton
onClick={() => {
setOpen(true);
}}
style={{ marginRight: '12px' }}
>
<LocalWorkspaceIcon />
</IconButton>
</Tooltip>
<TransformWorkspaceToAffineModal
open={open}
onClose={() => {
setOpen(false);
}}
onConform={async () => {
if (!getLoginStorage()) {
const response = await affineAuth.generateToken(
SignMethod.Google
);
if (response) {
setLoginStorage(response);
}
router.reload();
return;
}
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
const id = await transformWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
workspace as LocalWorkspace
);
// fixme(himself65): refactor this
await router.replace({
pathname: `/workspace/[workspaceId]/all`,
query: {
workspaceId: id,
},
});
setOpen(false);
router.reload();
}}
/>
</>
);
}
return (
<Tooltip content={t['Synced with AFFiNE Cloud']()} placement="bottom-end">
<IconWrapper>
<CloudWorkspaceIcon />
</IconWrapper>
</Tooltip>
);
};
export default SyncUser;

View File

@@ -3,7 +3,6 @@ import { AffineLogoSBlue2_1Icon, SignOutIcon } from '@blocksuite/icons';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { useCurrentUser } from '../../../../hooks/current/use-current-user';
const EditMenu = ( const EditMenu = (
<MenuItem data-testid="editor-option-menu-favorite" icon={<SignOutIcon />}> <MenuItem data-testid="editor-option-menu-favorite" icon={<SignOutIcon />}>
Sign Out Sign Out
@@ -11,7 +10,8 @@ const EditMenu = (
); );
export const UserAvatar = () => { export const UserAvatar = () => {
const user = useCurrentUser(); // fixme: cloud regression
const user: any = null;
return ( return (
<Menu <Menu
width={276} width={276}

View File

@@ -29,7 +29,6 @@ import { DownloadClientTip } from './download-tips';
import EditPage from './header-right-items/edit-page'; import EditPage from './header-right-items/edit-page';
import { EditorOptionMenu } from './header-right-items/editor-option-menu'; import { EditorOptionMenu } from './header-right-items/editor-option-menu';
import { HeaderShareMenu } from './header-right-items/share-menu'; import { HeaderShareMenu } from './header-right-items/share-menu';
import SyncUser from './header-right-items/sync-user';
import TrashButtonGroup from './header-right-items/trash-button-group'; import TrashButtonGroup from './header-right-items/trash-button-group';
import UserAvatar from './header-right-items/user-avatar'; import UserAvatar from './header-right-items/user-avatar';
import * as styles from './styles.css'; import * as styles from './styles.css';
@@ -47,7 +46,6 @@ export type BaseHeaderProps<
export enum HeaderRightItemName { export enum HeaderRightItemName {
EditorOptionMenu = 'editorOptionMenu', EditorOptionMenu = 'editorOptionMenu',
TrashButtonGroup = 'trashButtonGroup', TrashButtonGroup = 'trashButtonGroup',
SyncUser = 'syncUser',
ShareMenu = 'shareMenu', ShareMenu = 'shareMenu',
EditPage = 'editPage', EditPage = 'editPage',
UserAvatar = 'userAvatar', UserAvatar = 'userAvatar',
@@ -75,12 +73,6 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
return currentPage?.meta.trash === true; return currentPage?.meta.trash === true;
}, },
}, },
[HeaderRightItemName.SyncUser]: {
Component: SyncUser,
availableWhen: (_, currentPage, { isPublic }) => {
return !isPublic;
},
},
[HeaderRightItemName.ShareMenu]: { [HeaderRightItemName.ShareMenu]: {
Component: HeaderShareMenu, Component: HeaderShareMenu,
availableWhen: (workspace, currentPage) => { availableWhen: (workspace, currentPage) => {

View File

@@ -1,76 +1,34 @@
import { FlexWrapper } from '@affine/component';
import { IconButton } from '@affine/component';
import { Tooltip } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { AccessTokenMessage } from '@affine/workspace/affine/login'; import { CloudWorkspaceIcon } from '@blocksuite/icons';
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import type { CSSProperties } from 'react'; import { type CSSProperties, type FC, forwardRef } from 'react';
import type React from 'react';
import { forwardRef } from 'react';
import { openDisableCloudAlertModalAtom } from '../../../atoms'; import { openDisableCloudAlertModalAtom } from '../../../atoms';
import { stringToColour } from '../../../utils'; import { stringToColour } from '../../../utils';
import { StyledFooter, StyledSignInButton, StyleUserInfo } from './styles'; import { StyledFooter, StyledSignInButton } from './styles';
export type FooterProps = { export const Footer: FC = () => {
user: AccessTokenMessage | null;
onLogin: () => void;
onLogout: () => void;
};
export const Footer: React.FC<FooterProps> = ({ user, onLogin, onLogout }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const setOpen = useSetAtom(openDisableCloudAlertModalAtom); const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
return ( return (
<StyledFooter data-testid="workspace-list-modal-footer"> <StyledFooter data-testid="workspace-list-modal-footer">
{user && ( <StyledSignInButton
<> data-testid="sign-in-button"
<FlexWrapper> noBorder
<WorkspaceAvatar bold
size={40} icon={
name={user.name} <div className="circle">
avatar={user.avatar_url} <CloudWorkspaceIcon />
></WorkspaceAvatar> </div>
<StyleUserInfo> }
<p>{user.name}</p> onClick={async () => {
<p>{user.email}</p> if (!runtimeConfig.enableLegacyCloud) {
</StyleUserInfo> setOpen(true);
</FlexWrapper>
<Tooltip content={t['Sign out']()} disablePortal={true}>
<IconButton
data-testid="workspace-list-modal-sign-out"
onClick={() => {
onLogout();
}}
>
<SignOutIcon />
</IconButton>
</Tooltip>
</>
)}
{!user && (
<StyledSignInButton
data-testid="sign-in-button"
noBorder
bold
icon={
<div className="circle">
<CloudWorkspaceIcon />
</div>
} }
onClick={async () => { }}
if (!runtimeConfig.enableLegacyCloud) { >
setOpen(true); {t['Sign in']()}
} else { </StyledSignInButton>
onLogin();
}
}}
>
{t['Sign in']()}
</StyledSignInButton>
)}
</StyledFooter> </StyledFooter>
); );
}; };

View File

@@ -1,59 +0,0 @@
import { MessageCode, Messages } from '@affine/env/constant';
import { setLoginStorage, SignMethod } from '@affine/workspace/affine/login';
import { affineAuth } from '@affine/workspace/affine/shared';
import type { FC } from 'react';
import { memo, useEffect, useState } from 'react';
import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
import { toast } from '../../../utils';
declare global {
interface DocumentEventMap {
'affine-error': CustomEvent<{
code: keyof typeof Messages;
}>;
}
}
export const MessageCenter: FC = memo(function MessageCenter() {
const [popup, setPopup] = useState(false);
const onLogout = useAffineLogOut();
useEffect(() => {
const listener = (
event: CustomEvent<{
code: keyof typeof Messages;
}>
) => {
// fixme: need refactor
// - login and refresh refresh logic should be refactored
// - error message should be refactored
if (
!popup &&
(event.detail.code === MessageCode.refreshTokenError ||
event.detail.code === MessageCode.loginError)
) {
setPopup(true);
affineAuth
.generateToken(SignMethod.Google)
.then(response => {
if (response) {
setLoginStorage(response);
}
setPopup(false);
})
.catch(() => {
setPopup(false);
return onLogout();
});
} else {
toast(Messages[event.detail.code].message);
}
};
document.addEventListener('affine-error', listener);
return () => {
document.removeEventListener('affine-error', listener);
};
}, [onLogout, popup]);
return null;
});

View File

@@ -9,12 +9,11 @@ import {
import { ScrollableContainer } from '@affine/component'; import { ScrollableContainer } from '@affine/component';
import { WorkspaceList } from '@affine/component/workspace-list'; import { WorkspaceList } from '@affine/component/workspace-list';
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons'; import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
@@ -41,15 +40,12 @@ import {
interface WorkspaceModalProps { interface WorkspaceModalProps {
disabled?: boolean; disabled?: boolean;
user: AccessTokenMessage | null;
workspaces: AllWorkspace[]; workspaces: AllWorkspace[];
currentWorkspaceId: AllWorkspace['id'] | null; currentWorkspaceId: AllWorkspace['id'] | null;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onClickWorkspace: (workspace: AllWorkspace) => void; onClickWorkspace: (workspace: AllWorkspace) => void;
onClickWorkspaceSetting: (workspace: AllWorkspace) => void; onClickWorkspaceSetting: (workspace: AllWorkspace) => void;
onClickLogin: () => void;
onClickLogout: () => void;
onNewWorkspace: () => void; onNewWorkspace: () => void;
onAddWorkspace: () => void; onAddWorkspace: () => void;
onMoveWorkspace: (activeId: string, overId: string) => void; onMoveWorkspace: (activeId: string, overId: string) => void;
@@ -161,9 +157,6 @@ export const WorkspaceListModal = ({
open, open,
onClose, onClose,
workspaces, workspaces,
user,
onClickLogin,
onClickLogout,
onClickWorkspace, onClickWorkspace,
onClickWorkspaceSetting, onClickWorkspaceSetting,
onNewWorkspace, onNewWorkspace,
@@ -213,7 +206,7 @@ export const WorkspaceListModal = ({
items={ items={
workspaces.filter( workspaces.filter(
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC ({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
) as (AffineLegacyCloudWorkspace | LocalWorkspace)[] ) as (AffineCloudWorkspace | LocalWorkspace)[]
} }
currentWorkspaceId={currentWorkspaceId} currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace} onClick={onClickWorkspace}
@@ -234,7 +227,7 @@ export const WorkspaceListModal = ({
/> />
</StyledModalContent> </StyledModalContent>
</ScrollableContainer> </ScrollableContainer>
<Footer user={user} onLogin={onClickLogin} onLogout={onClickLogout} /> <Footer />
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>
); );

View File

@@ -11,13 +11,11 @@ import {
SidebarScrollableContainer, SidebarScrollableContainer,
} from '@affine/component/app-sidebar'; } from '@affine/component/app-sidebar';
import { isDesktop } from '@affine/env/constant'; import { isDesktop } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import {
DeleteTemporarilyIcon, DeleteTemporarilyIcon,
FolderIcon, FolderIcon,
SettingsIcon, SettingsIcon,
ShareIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
@@ -199,25 +197,6 @@ export const RootAppSidebar = ({
{blockSuiteWorkspace && ( {blockSuiteWorkspace && (
<FavoriteList currentWorkspace={currentWorkspace} /> <FavoriteList currentWorkspace={currentWorkspace} />
)} )}
{runtimeConfig.enableLegacyCloud &&
(currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
currentWorkspace.public ? (
<RouteMenuLinkItem
icon={<ShareIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
>
<span data-testid="Published-to-web">Published to web</span>
</RouteMenuLinkItem>
) : (
<RouteMenuLinkItem
icon={<ShareIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.shared(currentWorkspaceId)}
>
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
</RouteMenuLinkItem>
))}
<CategoryDivider label={t['Collections']()} /> <CategoryDivider label={t['Collections']()} />
{blockSuiteWorkspace && ( {blockSuiteWorkspace && (
<CollectionsList currentWorkspace={currentWorkspace} /> <CollectionsList currentWorkspace={currentWorkspace} />

View File

@@ -1,16 +0,0 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { WorkspaceAdapters } from '../../adapters/workspace';
export function useAffineLogIn() {
const router = useRouter();
return useCallback(async () => {
await WorkspaceAdapters[WorkspaceFlavour.AFFINE].Events[
'workspace:access'
]?.();
// todo: remove reload page requirement
router.reload();
}, [router]);
}

View File

@@ -1,15 +0,0 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { WorkspaceAdapters } from '../../adapters/workspace';
export function useAffineLogOut() {
const router = useRouter();
return useCallback(async () => {
await WorkspaceAdapters[WorkspaceFlavour.AFFINE].Events[
'workspace:revoke'
]?.();
router.reload();
}, [router]);
}

View File

@@ -1,50 +0,0 @@
import { DebugLogger } from '@affine/debug';
import {
getLoginStorage,
isExpired,
parseIdToken,
setLoginStorage,
storageChangeSlot,
} from '@affine/workspace/affine/login';
import { affineAuth } from '@affine/workspace/affine/shared';
import useSWR from 'swr';
const logger = new DebugLogger('auth-token');
const revalidate = async () => {
const storage = getLoginStorage();
if (storage) {
try {
const tokenMessage = parseIdToken(storage.token);
logger.debug('revalidate affine user');
if (isExpired(tokenMessage)) {
logger.debug('need to refresh token');
const response = await affineAuth.refreshToken(storage);
if (response) {
setLoginStorage(response);
storageChangeSlot.emit();
}
}
} catch (e) {
return false;
}
return true;
} else {
return false;
}
};
export function useAffineRefreshAuthToken(
// every 30 seconds, check if the token is expired
refreshInterval = 30 * 1000
) {
const { data } = useSWR<boolean>('autoRefreshToken', {
suspense: true,
fetcher: revalidate,
refreshInterval,
revalidateOnFocus: true,
revalidateOnReconnect: true,
revalidateOnMount: true,
});
return data;
}

View File

@@ -1,10 +0,0 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
import type { AffineOfficialWorkspace } from '../../shared';
export function useIsWorkspaceOwner(workspace: AffineOfficialWorkspace) {
if (workspace.flavour === WorkspaceFlavour.LOCAL) return true;
if (workspace.flavour === WorkspaceFlavour.PUBLIC) return false;
return workspace.permission === PermissionType.Owner;
}

View File

@@ -1,42 +0,0 @@
import type { Member } from '@affine/env/workspace/legacy-cloud';
import { affineApis } from '@affine/workspace/affine/shared';
import { useCallback } from 'react';
import useSWR from 'swr';
import { QueryKey } from '../../adapters/affine/fetcher';
export function useMembers(workspaceId: string) {
const { data, mutate } = useSWR<Member[]>(
[QueryKey.getMembers, workspaceId],
{
fallbackData: [],
}
);
const inviteMember = useCallback(
async (email: string) => {
await affineApis.inviteMember({
id: workspaceId,
email,
});
return mutate();
},
[mutate, workspaceId]
);
const removeMember = useCallback(
async (permissionId: number) => {
await affineApis.removeMember({
permissionId,
});
return mutate();
},
[mutate]
);
return {
members: data ?? [],
inviteMember,
removeMember,
};
}

View File

@@ -1,26 +0,0 @@
import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace';
import { affineApis } from '@affine/workspace/affine/shared';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { useCallback } from 'react';
import useSWR from 'swr';
import { QueryKey } from '../../adapters/affine/fetcher';
export function useToggleWorkspacePublish(
workspace: AffineLegacyCloudWorkspace
) {
const { mutate } = useSWR(QueryKey.getWorkspaces);
return useCallback(
async (isPublish: boolean) => {
await affineApis.updateWorkspace({
id: workspace.id,
public: isPublish,
});
await mutate(QueryKey.getWorkspaces);
// fixme: remove force update
await rootStore.set(rootWorkspacesMetadataAtom, ws => [...ws]);
},
[mutate, workspace.id]
);
}

View File

@@ -1,24 +0,0 @@
import useSWR from 'swr';
import { QueryKey } from '../../adapters/affine/fetcher';
export interface QueryEmailMember {
id: string;
name: string;
email: string;
avatar_url: string;
create_at: string;
}
export function useUsersByEmail(
workspaceId: string,
email: string
): QueryEmailMember[] | null {
const { data } = useSWR<QueryEmailMember[] | null>(
[QueryKey.getUserByEmail, workspaceId, email],
{
fallbackData: null,
}
);
return data ?? null;
}

View File

@@ -1,7 +0,0 @@
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import { useAtomValue } from 'jotai';
export function useCurrentUser(): AccessTokenMessage | null {
return useAtomValue(currentAffineUserAtom);
}

View File

@@ -1,14 +1,5 @@
import type { WorkspaceRegistry } from '@affine/env/workspace'; import type { WorkspaceRegistry } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
getLoginStorage,
parseIdToken,
setLoginStorage,
SignMethod,
storageChangeSlot,
} from '@affine/workspace/affine/login';
import { affineAuth } from '@affine/workspace/affine/shared';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom'; import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
@@ -17,7 +8,6 @@ import { useTransformWorkspace } from '../use-transform-workspace';
export function useOnTransformWorkspace() { export function useOnTransformWorkspace() {
const transformWorkspace = useTransformWorkspace(); const transformWorkspace = useTransformWorkspace();
const setUser = useSetAtom(currentAffineUserAtom);
const setWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom); const setWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
return useCallback( return useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>( async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
@@ -25,15 +15,6 @@ export function useOnTransformWorkspace() {
to: To, to: To,
workspace: WorkspaceRegistry[From] workspace: WorkspaceRegistry[From]
): Promise<void> => { ): Promise<void> => {
const needRefresh = to === WorkspaceFlavour.AFFINE && !getLoginStorage();
if (needRefresh) {
const response = await affineAuth.generateToken(SignMethod.Google);
if (response) {
setLoginStorage(response);
setUser(parseIdToken(response.token));
storageChangeSlot.emit();
}
}
const workspaceId = await transformWorkspace(from, to, workspace); const workspaceId = await transformWorkspace(from, to, workspace);
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('affine-workspace:transform', { new CustomEvent('affine-workspace:transform', {
@@ -47,7 +28,7 @@ export function useOnTransformWorkspace() {
); );
setWorkspaceId(workspaceId); setWorkspaceId(workspaceId);
}, },
[setUser, setWorkspaceId, transformWorkspace] [setWorkspaceId, transformWorkspace]
); );
} }

View File

@@ -43,10 +43,6 @@ import {
openWorkspacesModalAtom, openWorkspacesModalAtom,
} from '../atoms'; } from '../atoms';
import { useTrackRouterHistoryEffect } from '../atoms/history'; import { useTrackRouterHistoryEffect } from '../atoms/history';
import {
publicWorkspaceAtom,
publicWorkspaceIdAtom,
} from '../atoms/public-workspace';
import { AppContainer } from '../components/affine/app-container'; import { AppContainer } from '../components/affine/app-container';
import type { IslandItemNames } from '../components/pure/help-island'; import type { IslandItemNames } from '../components/pure/help-island';
import { HelpIsland } from '../components/pure/help-island'; import { HelpIsland } from '../components/pure/help-island';
@@ -78,24 +74,6 @@ const SettingModal = lazy(() =>
})) }))
); );
export const PublicQuickSearch: FC = () => {
const publicWorkspace = useAtomValue(publicWorkspaceAtom);
const router = useRouter();
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
openQuickSearchModalAtom
);
return (
<Suspense>
<QuickSearchModal
blockSuiteWorkspace={publicWorkspace.blockSuiteWorkspace}
open={openQuickSearchModal}
setOpen={setOpenQuickSearchModalAtom}
router={router}
/>
</Suspense>
);
};
function DefaultProvider({ children }: PropsWithChildren) { function DefaultProvider({ children }: PropsWithChildren) {
return <>{children}</>; return <>{children}</>;
} }
@@ -107,13 +85,7 @@ export const QuickSearch: FC = () => {
openQuickSearchModalAtom openQuickSearchModalAtom
); );
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace; const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const isPublicWorkspace =
router.pathname.split('/')[1] === 'public-workspace';
const publicWorkspaceId = useAtomValue(publicWorkspaceIdAtom);
if (!blockSuiteWorkspace) { if (!blockSuiteWorkspace) {
if (isPublicWorkspace && publicWorkspaceId) {
return <PublicQuickSearch />;
}
return null; return null;
} }
return ( return (
@@ -127,13 +99,10 @@ export const QuickSearch: FC = () => {
}; };
export const Setting: FC = () => { export const Setting: FC = () => {
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const router = useRouter();
const [openSettingModal, setOpenSettingModalAtom] = const [openSettingModal, setOpenSettingModalAtom] =
useAtom(openSettingModalAtom); useAtom(openSettingModalAtom);
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace; const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const isPublicWorkspace = if (!blockSuiteWorkspace) {
router.pathname.split('/')[1] === 'public-workspace';
if (!blockSuiteWorkspace || isPublicWorkspace) {
return null; return null;
} }
return ( return (

View File

@@ -15,7 +15,6 @@ import type { PropsWithChildren, ReactElement } from 'react';
import React, { lazy, Suspense, useEffect } from 'react'; import React, { lazy, Suspense, useEffect } from 'react';
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary'; import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
import { MessageCenter } from '../components/pure/message-center';
import type { NextPageWithLayout } from '../shared'; import type { NextPageWithLayout } from '../shared';
import createEmotionCache from '../utils/create-emotion-cache'; import createEmotionCache from '../utils/create-emotion-cache';
@@ -61,7 +60,6 @@ const App = function App({
return ( return (
<CacheProvider value={emotionCache}> <CacheProvider value={emotionCache}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<MessageCenter />
<AffineErrorBoundary router={useRouter()}> <AffineErrorBoundary router={useRouter()}>
<AffineContext> <AffineContext>
<Head> <Head>

View File

@@ -1,105 +0,0 @@
import { Button } from '@affine/component';
import { MainContainer } from '@affine/component/workspace';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
clearLoginStorage,
createAffineAuth,
getLoginStorage,
isExpired,
parseIdToken,
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import { useAtom } from 'jotai';
import type { NextPage } from 'next';
import { lazy, Suspense, useMemo } from 'react';
import { AppContainer } from '../../components/affine/app-container';
import { toast } from '../../utils';
const Viewer = lazy(() =>
import('@rich-data/viewer').then(m => ({ default: m.JsonViewer }))
);
import { useTheme } from 'next-themes';
const LoginDevPage: NextPage = () => {
const [user, setUser] = useAtom(currentAffineUserAtom);
const auth = useMemo(() => createAffineAuth(), []);
return (
<AppContainer>
<MainContainer>
<h1>LoginDevPage</h1>
<Button
onClick={async () => {
const storage = getLoginStorage();
if (storage) {
const user = parseIdToken(storage.token);
if (isExpired(user)) {
await auth.refreshToken(storage);
}
}
const response = await auth.generateToken(SignMethod.Google);
if (response) {
setLoginStorage(response);
const user = parseIdToken(response.token);
setUser(user);
} else {
toast('Login failed');
}
}}
>
Login
</Button>
<Button
onClick={async () => {
const storage = getLoginStorage();
if (!storage) {
throw new Error('No storage');
}
const response = await auth.refreshToken(storage);
if (response) {
setLoginStorage(response);
const user = parseIdToken(response.token);
setUser(user);
} else {
toast('Login failed');
}
}}
>
Refresh Token
</Button>
<Button
onClick={() => {
clearLoginStorage();
setUser(null);
}}
>
Reset Storage
</Button>
<Button
onClick={async () => {
const status = await fetch('/api/workspace', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
Authorization: getLoginStorage()?.token ?? '',
},
}).then(r => r.status);
toast(`Response Status: ${status}`);
}}
>
Check Permission
</Button>
<Suspense>
<Viewer
theme={useTheme().resolvedTheme === 'light' ? 'light' : 'dark'}
value={user}
/>
</Suspense>
</MainContainer>
</AppContainer>
);
};
export default LoginDevPage;

View File

@@ -1,112 +0,0 @@
import { displayFlex, styled } from '@affine/component';
import { Button } from '@affine/component';
import { WorkspaceSubPath } from '@affine/env/workspace';
import type { Permission } from '@affine/env/workspace/legacy-cloud';
import {
SucessfulDuotoneIcon,
UnsucessfulDuotoneIcon,
} from '@blocksuite/icons';
import { NoSsr } from '@mui/material';
import Image from 'next/legacy/image';
import { useRouter } from 'next/router';
import { Suspense } from 'react';
import useSWR from 'swr';
import { QueryKey } from '../../adapters/affine/fetcher';
import { PageLoading } from '../../components/pure/loading';
import { RouteLogic, useRouterHelper } from '../../hooks/use-router-helper';
import type { NextPageWithLayout } from '../../shared';
const InvitePage: NextPageWithLayout = () => {
const router = useRouter();
const { jumpToSubPath } = useRouterHelper(router);
const { data: inviteData } = useSWR<Permission>(
typeof router.query.invite_code === 'string'
? [QueryKey.acceptInvite, router.query.invite_code]
: null
);
if (inviteData?.accepted) {
return (
<StyledContainer>
<Image
src="/imgs/invite-success.svg"
alt=""
layout="fill"
width={300}
height={300}
/>
<Button
type="primary"
shape="round"
onClick={() => {
jumpToSubPath(
inviteData.workspace_id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
).catch(err => console.error(err));
}}
>
Go to Workspace
</Button>
<p>
<SucessfulDuotoneIcon />
Successfully joined
</p>
</StyledContainer>
);
}
if (inviteData?.accepted === false) {
return (
<StyledContainer>
<Image src="/imgs/invite-error.svg" alt="" />
<Button
shape="round"
onClick={() => {
router.replace(`/`).catch(err => console.error(err));
}}
>
Back to Home
</Button>
<p>
<UnsucessfulDuotoneIcon />
The link has expired
</p>
</StyledContainer>
);
}
throw new Error('Invalid invite code');
};
export default InvitePage;
InvitePage.getLayout = page => {
return (
<Suspense fallback={<PageLoading />}>
<NoSsr>{page}</NoSsr>
</Suspense>
);
};
const StyledContainer = styled('div')(() => {
return {
height: '100vh',
...displayFlex('center', 'center'),
flexDirection: 'column',
backgroundColor: 'var(--affine-background-primary-color)',
img: {
width: '300px',
height: '300px',
},
p: {
...displayFlex('center', 'center'),
marginTop: '24px',
svg: {
color: 'var(--affine-primary-color)',
fontSize: '24px',
marginRight: '12px',
},
},
};
});

View File

@@ -1,134 +0,0 @@
import { Breadcrumbs, IconButton, ListSkeleton } from '@affine/component';
import { StyledTableContainer } from '@affine/component/page-list';
import { QueryParamError } from '@affine/env/constant';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import { SearchIcon } from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useAtomValue, useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import { lazy, Suspense, useCallback, useEffect } from 'react';
import { openQuickSearchModalAtom } from '../../atoms';
import {
publicWorkspaceAtom,
publicWorkspaceIdAtom,
} from '../../atoms/public-workspace';
import { WorkspaceAvatar } from '../../components/pure/footer';
import { PageLoading } from '../../components/pure/loading';
import {
PublicQuickSearch,
PublicWorkspaceLayout,
} from '../../layouts/public-workspace-layout';
import type { NextPageWithLayout } from '../../shared';
import { NavContainer, StyledBreadcrumbs } from './[workspaceId]/[pageId]';
const BlockSuitePageList = lazy(() =>
import('../../components/blocksuite/block-suite-page-list').then(module => ({
default: module.BlockSuitePageList,
}))
);
const ListPageInner: React.FC<{
workspaceId: string;
}> = ({ workspaceId }) => {
const router = useRouter();
const publicWorkspace = useAtomValue(publicWorkspaceAtom);
const blockSuiteWorkspace = publicWorkspace.blockSuiteWorkspace;
const handleClickPage = useCallback(
(pageId: string) => {
return router.push({
pathname: `/public-workspace/[workspaceId]/[pageId]`,
query: {
workspaceId,
pageId,
},
});
},
[router, workspaceId]
);
const [name] = useBlockSuiteWorkspaceName(blockSuiteWorkspace);
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(blockSuiteWorkspace);
const setSearchModalOpen = useSetAtom(openQuickSearchModalAtom);
const handleOpen = useCallback(() => {
setSearchModalOpen(true);
}, [setSearchModalOpen]);
if (!blockSuiteWorkspace) {
return <PageLoading />;
}
return (
<>
<PublicQuickSearch workspace={publicWorkspace} />
<NavContainer sx={{ px: '20px' }}>
<Breadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${blockSuiteWorkspace.id}`}
>
<WorkspaceAvatar size={24} name={name} avatar={avatar} />
<span>{name}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
<IconButton onClick={handleOpen}>
<SearchIcon />
</IconButton>
</NavContainer>
<Suspense
fallback={
<StyledTableContainer>
<ListSkeleton />
</StyledTableContainer>
}
>
<BlockSuitePageList
listType="public"
isPublic={true}
onOpenPage={handleClickPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
</Suspense>
</>
);
};
// This is affine only page, so we don't need to dynamic use WorkspacePlugin
const ListPage: NextPageWithLayout = () => {
const router = useRouter();
const workspaceId = router.query.workspaceId;
const setWorkspaceId = useSetAtom(publicWorkspaceIdAtom);
// todo: remove this atom usage here
const setCurrentWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (typeof workspaceId === 'string') {
setWorkspaceId(workspaceId);
setCurrentWorkspaceId(workspaceId);
}
}, [router.isReady, setCurrentWorkspaceId, setWorkspaceId, workspaceId]);
const value = useAtomValue(publicWorkspaceIdAtom);
if (!router.isReady || !value) {
return <PageLoading />;
}
if (typeof workspaceId !== 'string') {
throw new QueryParamError('workspaceId', workspaceId);
}
return (
<Suspense
fallback={
<StyledTableContainer>
<ListSkeleton />
</StyledTableContainer>
}
>
<ListPageInner workspaceId={workspaceId} />
</Suspense>
);
};
export default ListPage;
ListPage.getLayout = page => {
return <PublicWorkspaceLayout>{page}</PublicWorkspaceLayout>;
};

View File

@@ -1,158 +0,0 @@
import { Breadcrumbs, displayFlex, styled } from '@affine/component';
import { initEmptyPage } from '@affine/env/blocksuite';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { PageIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useAtom, useAtomValue } from 'jotai';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import { Suspense, useCallback, useEffect } from 'react';
import {
publicPageBlockSuiteAtom,
publicWorkspaceIdAtom,
publicWorkspacePageIdAtom,
} from '../../../atoms/public-workspace';
import { BlockSuiteEditorHeader } from '../../../components/blocksuite/workspace-header';
import {
PageDetailEditor,
type PageDetailEditorProps,
} from '../../../components/page-detail-editor';
import { WorkspaceAvatar } from '../../../components/pure/footer';
import { PageLoading } from '../../../components/pure/loading';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import {
PublicQuickSearch,
PublicWorkspaceLayout,
} from '../../../layouts/public-workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
export const NavContainer = styled('div')(() => {
return {
width: '100vw',
height: '52px',
...displayFlex('space-between', 'center'),
backgroundColor: 'var(--affine-background-primary-color)',
};
});
export const StyledBreadcrumbs = styled(Link)(() => {
return {
flex: 1,
...displayFlex('center', 'center'),
paddingLeft: '12px',
span: {
padding: '0 12px',
fontSize: 'var(--affine-font-base)',
lineHeight: 'var(--affine-line-height)',
},
':hover': { color: 'var(--affine-primary-color)' },
transition: 'all .15s',
':visited': {
':hover': { color: 'var(--affine-primary-color)' },
},
};
});
const PublicWorkspaceDetailPageInner = (): ReactElement => {
const pageId = useAtomValue(publicWorkspacePageIdAtom);
assertExists(pageId, 'pageId is null');
const publicWorkspace = useAtomValue(publicPageBlockSuiteAtom);
const blockSuiteWorkspace = publicWorkspace.blockSuiteWorkspace;
if (!blockSuiteWorkspace) {
throw new Error('cannot find workspace');
}
const router = useRouter();
const { openPage } = useRouterHelper(router);
const t = useAFFiNEI18N();
const [name] = useBlockSuiteWorkspaceName(blockSuiteWorkspace);
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(blockSuiteWorkspace);
const pageTitle = blockSuiteWorkspace.meta.getPageMeta(pageId)?.title;
const onLoad = useCallback<NonNullable<PageDetailEditorProps['onLoad']>>(
(_, editor) => {
const { page } = editor;
page.awarenessStore.setReadonly(page, true);
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
return openPage(blockSuiteWorkspace.id, pageId);
});
return () => {
dispose.dispose();
};
},
[blockSuiteWorkspace.id, openPage]
);
return (
<>
<PublicQuickSearch workspace={publicWorkspace} />
<BlockSuiteEditorHeader
isPublic={true}
workspace={publicWorkspace}
currentPage={blockSuiteWorkspace.getPage(pageId)}
>
<NavContainer>
<Breadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${blockSuiteWorkspace.id}`}
>
<WorkspaceAvatar size={24} name={name} avatar={avatar} />
<span>{name}</span>
</StyledBreadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${blockSuiteWorkspace.id}/${pageId}`}
>
<PageIcon fontSize={24} />
<span>{pageTitle ? pageTitle : t['Untitled']()}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
</NavContainer>
</BlockSuiteEditorHeader>
<PageDetailEditor
isPublic={true}
pageId={pageId}
workspace={publicWorkspace}
onLoad={onLoad}
onInit={initEmptyPage}
/>
</>
);
};
export const PublicWorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter();
const [workspaceId, setWorkspaceId] = useAtom(publicWorkspaceIdAtom);
const [pageId, setPageId] = useAtom(publicWorkspacePageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (typeof router.query.workspaceId === 'string') {
setWorkspaceId(router.query.workspaceId);
}
if (typeof router.query.pageId === 'string') {
setPageId(router.query.pageId);
}
}, [
router.isReady,
router.query.pageId,
router.query.workspaceId,
setPageId,
setWorkspaceId,
]);
if (!router.isReady || !workspaceId || !pageId) {
return <PageLoading />;
}
return (
<Suspense fallback={<PageLoading />}>
<PublicWorkspaceDetailPageInner />
</Suspense>
);
};
export default PublicWorkspaceDetailPage;
PublicWorkspaceDetailPage.getLayout = page => {
return <PublicWorkspaceLayout>{page}</PublicWorkspaceLayout>;
};

View File

@@ -1,136 +0,0 @@
import type { SettingPanel } from '@affine/env/workspace';
import {
settingPanel,
settingPanelValues,
WorkspaceSubPath,
} from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/store';
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import Head from 'next/head';
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { getUIAdapter } from '../../../adapters/workspace';
import { PageLoading } from '../../../components/pure/loading';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
import { useAppHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
const settingPanelAtom = atomWithStorage<SettingPanel>(
'workspaceId',
settingPanel.General
);
function useTabRouterSync(
router: NextRouter,
currentTab: SettingPanel,
setCurrentTab: (tab: SettingPanel) => void
): void {
if (!router.isReady) {
return;
}
const queryCurrentTab =
typeof router.query.currentTab === 'string'
? router.query.currentTab
: null;
if (
(queryCurrentTab !== null &&
settingPanelValues.indexOf(queryCurrentTab as SettingPanel) === -1) ||
settingPanelValues.indexOf(currentTab as SettingPanel) === -1
) {
setCurrentTab(settingPanel.General);
router
.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: settingPanel.General,
},
})
.catch(console.error);
} else if (queryCurrentTab !== currentTab) {
router
.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: currentTab,
},
})
.catch(console.error);
}
}
const SettingPage: NextPageWithLayout = () => {
const router = useRouter();
const [currentWorkspace] = useCurrentWorkspace();
const t = useAFFiNEI18N();
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
const onChangeTab = useCallback(
(tab: SettingPanel) => {
setCurrentTab(tab as SettingPanel);
router
.push({
pathname: router.pathname,
query: {
...router.query,
currentTab: tab,
},
})
.catch(err => {
console.error(err);
});
},
[router, setCurrentTab]
);
useTabRouterSync(router, currentTab, setCurrentTab);
const helper = useAppHelper();
const onDeleteWorkspace = useCallback(async () => {
assertExists(currentWorkspace);
const workspaceId = currentWorkspace.id;
return helper.deleteWorkspace(workspaceId);
}, [currentWorkspace, helper]);
const onTransformWorkspace = useOnTransformWorkspace();
if (
!router.isReady ||
currentWorkspace === null ||
settingPanelValues.indexOf(currentTab as SettingPanel) === -1
) {
return <PageLoading />;
}
const { SettingsDetail, Header } = getUIAdapter(currentWorkspace.flavour);
return (
<>
<Head>
<title>{t['Settings']()} - AFFiNE</title>
</Head>
<Header
currentWorkspace={currentWorkspace}
currentEntry={{
subPath: WorkspaceSubPath.SETTING,
}}
/>
<SettingsDetail
onTransformWorkspace={onTransformWorkspace}
onDeleteWorkspace={onDeleteWorkspace}
currentWorkspace={currentWorkspace}
currentTab={currentTab as SettingPanel}
onChangeTab={onChangeTab}
/>
</>
);
};
export default SettingPage;
SettingPage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};

View File

@@ -1,17 +0,0 @@
import type React from 'react';
import { memo } from 'react';
import type { SWRConfiguration } from 'swr';
import { SWRConfig } from 'swr';
import { fetcher } from '../adapters/affine/fetcher';
const config: SWRConfiguration = {
suspense: true,
fetcher,
};
export const AffineSwrConfigProvider = memo<React.PropsWithChildren>(
function AffineSWRConfigProvider({ children }) {
return <SWRConfig value={config}>{children}</SWRConfig>;
}
);

View File

@@ -15,9 +15,6 @@ import {
openOnboardingModalAtom, openOnboardingModalAtom,
openWorkspacesModalAtom, openWorkspacesModalAtom,
} from '../atoms'; } from '../atoms';
import { useAffineLogIn } from '../hooks/affine/use-affine-log-in';
import { useAffineLogOut } from '../hooks/affine/use-affine-log-out';
import { useCurrentUser } from '../hooks/current/use-current-user';
import { useRouterHelper } from '../hooks/use-router-helper'; import { useRouterHelper } from '../hooks/use-router-helper';
import { useWorkspaces } from '../hooks/use-workspaces'; import { useWorkspaces } from '../hooks/use-workspaces';
@@ -90,7 +87,6 @@ export const AllWorkspaceModals = (): ReactElement => {
const router = useRouter(); const router = useRouter();
const { jumpToSubPath } = useRouterHelper(router); const { jumpToSubPath } = useRouterHelper(router);
const user = useCurrentUser();
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom); const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom( const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
@@ -102,7 +98,6 @@ export const AllWorkspaceModals = (): ReactElement => {
<Suspense> <Suspense>
<WorkspaceListModal <WorkspaceListModal
disabled={transitioning} disabled={transitioning}
user={user}
workspaces={workspaces} workspaces={workspaces}
currentWorkspaceId={currentWorkspaceId} currentWorkspaceId={currentWorkspaceId}
open={ open={
@@ -146,8 +141,6 @@ export const AllWorkspaceModals = (): ReactElement => {
}, },
[jumpToSubPath, setCurrentWorkspaceId, setOpenWorkspacesModal] [jumpToSubPath, setCurrentWorkspaceId, setOpenWorkspacesModal]
)} )}
onClickLogin={useAffineLogIn()}
onClickLogout={useAffineLogOut()}
onNewWorkspace={useCallback(() => { onNewWorkspace={useCallback(() => {
setOpenCreateWorkspaceModal('new'); setOpenCreateWorkspaceModal('new');
}, [setOpenCreateWorkspaceModal])} }, [setOpenCreateWorkspaceModal])}

View File

@@ -1,5 +1,5 @@
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import type { AffinePublicWorkspace } from '@affine/env/workspace'; import type { AffinePublicWorkspace } from '@affine/env/workspace';
@@ -11,7 +11,7 @@ import type { ReactElement, ReactNode } from 'react';
export { BlockSuiteWorkspace }; export { BlockSuiteWorkspace };
export type AffineOfficialWorkspace = export type AffineOfficialWorkspace =
| AffineLegacyCloudWorkspace | AffineCloudWorkspace
| LocalWorkspace | LocalWorkspace
| AffinePublicWorkspace; | AffinePublicWorkspace;

View File

@@ -1,9 +1,8 @@
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SettingsIcon } from '@blocksuite/icons'; import { SettingsIcon } from '@blocksuite/icons';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
@@ -19,7 +18,7 @@ import {
} from './styles'; } from './styles';
export type WorkspaceTypeProps = { export type WorkspaceTypeProps = {
workspace: AffineLegacyCloudWorkspace | LocalWorkspace; workspace: AffineCloudWorkspace | LocalWorkspace;
}; };
import { import {
@@ -27,7 +26,6 @@ import {
CollaborationIcon as DefaultJoinedWorkspaceIcon, CollaborationIcon as DefaultJoinedWorkspaceIcon,
LocalDataIcon as DefaultLocalDataIcon, LocalDataIcon as DefaultLocalDataIcon,
LocalWorkspaceIcon as DefaultLocalWorkspaceIcon, LocalWorkspaceIcon as DefaultLocalWorkspaceIcon,
PublishIcon as DefaultPublishIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
const JoinedWorkspaceIcon = () => { const JoinedWorkspaceIcon = () => {
@@ -44,18 +42,11 @@ const CloudWorkspaceIcon = () => {
const LocalDataIcon = () => { const LocalDataIcon = () => {
return <DefaultLocalDataIcon style={{ color: '#62CD80' }} />; return <DefaultLocalDataIcon style={{ color: '#62CD80' }} />;
}; };
const PublishIcon = () => {
return <DefaultPublishIcon style={{ color: '#8699FF' }} />;
};
const WorkspaceType: FC<WorkspaceTypeProps> = ({ workspace }) => { const WorkspaceType: FC<WorkspaceTypeProps> = ({ workspace }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
let isOwner = true; // fixme: cloud regression
if (workspace.flavour === WorkspaceFlavour.AFFINE) { const isOwner = true;
isOwner = workspace.permission === PermissionType.Owner;
} else if (workspace.flavour === WorkspaceFlavour.LOCAL) {
isOwner = true;
}
if (workspace.flavour === WorkspaceFlavour.LOCAL) { if (workspace.flavour === WorkspaceFlavour.LOCAL) {
return ( return (
@@ -81,11 +72,9 @@ const WorkspaceType: FC<WorkspaceTypeProps> = ({ workspace }) => {
export type WorkspaceCardProps = { export type WorkspaceCardProps = {
currentWorkspaceId: string | null; currentWorkspaceId: string | null;
workspace: AffineLegacyCloudWorkspace | LocalWorkspace; workspace: AffineCloudWorkspace | LocalWorkspace;
onClick: (workspace: AffineLegacyCloudWorkspace | LocalWorkspace) => void; onClick: (workspace: AffineCloudWorkspace | LocalWorkspace) => void;
onSettingClick: ( onSettingClick: (workspace: AffineCloudWorkspace | LocalWorkspace) => void;
workspace: AffineLegacyCloudWorkspace | LocalWorkspace
) => void;
}; };
export const WorkspaceCard: FC<WorkspaceCardProps> = ({ export const WorkspaceCard: FC<WorkspaceCardProps> = ({
@@ -116,12 +105,12 @@ export const WorkspaceCard: FC<WorkspaceCardProps> = ({
<span>{t['Available Offline']()}</span> <span>{t['Available Offline']()}</span>
</p> </p>
)} )}
{workspace.flavour === WorkspaceFlavour.AFFINE && workspace.public && ( {/* {workspace.flavour === WorkspaceFlavour.AFFINE && workspace.public && (
<p title={t['Published to Web']()}> <p title={t['Published to Web']()}>
<PublishIcon /> <PublishIcon />
<span>{t['Published to Web']()}</span> <span>{t['Published to Web']()}</span>
</p> </p>
)} )} */}
</StyleWorkspaceInfo> </StyleWorkspaceInfo>
<StyledSettingLink <StyledSettingLink
className="setting-entry" className="setting-entry"

View File

@@ -1,5 +1,5 @@
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { ExportIcon, PublishIcon, ShareIcon } from '@blocksuite/icons'; import { ExportIcon, PublishIcon, ShareIcon } from '@blocksuite/icons';
@@ -27,8 +27,8 @@ const tabIcons = {
ShareWorkspace: <PublishIcon />, ShareWorkspace: <PublishIcon />,
}; };
export type ShareMenuProps< export type ShareMenuProps<
Workspace extends AffineLegacyCloudWorkspace | LocalWorkspace = Workspace extends AffineCloudWorkspace | LocalWorkspace =
| AffineLegacyCloudWorkspace | AffineCloudWorkspace
| LocalWorkspace | LocalWorkspace
> = { > = {
workspace: Workspace; workspace: Workspace;

View File

@@ -124,7 +124,7 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
export const SharePage: FC<ShareMenuProps> = props => { export const SharePage: FC<ShareMenuProps> = props => {
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <LocalSharePage {...props} />; return <LocalSharePage {...props} />;
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE) { } else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return <AffineSharePage {...props} />; return <AffineSharePage {...props} />;
} }
throw new Error('Unreachable'); throw new Error('Unreachable');

View File

@@ -1,5 +1,5 @@
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
@@ -30,9 +30,10 @@ const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
}; };
const ShareAffineWorkspace: FC< const ShareAffineWorkspace: FC<
ShareMenuProps<AffineLegacyCloudWorkspace> ShareMenuProps<AffineCloudWorkspace>
> = props => { > = props => {
const isPublicWorkspace = props.workspace.public; // fixme: regression
const isPublicWorkspace = false;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( return (
<div className={menuItemStyle}> <div className={menuItemStyle}>
@@ -58,10 +59,10 @@ export const ShareWorkspace: FC<ShareMenuProps> = props => {
return ( return (
<ShareLocalWorkspace {...(props as ShareMenuProps<LocalWorkspace>)} /> <ShareLocalWorkspace {...(props as ShareMenuProps<LocalWorkspace>)} />
); );
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE) { } else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return ( return (
<ShareAffineWorkspace <ShareAffineWorkspace
{...(props as ShareMenuProps<AffineLegacyCloudWorkspace>)} {...(props as ShareMenuProps<AffineCloudWorkspace>)}
/> />
); );
} }

View File

@@ -1,5 +1,5 @@
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
AffinePublicWorkspace, AffinePublicWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
@@ -16,7 +16,7 @@ import { avatarImageStyle, avatarStyle } from './index.css';
export type WorkspaceAvatarProps = { export type WorkspaceAvatarProps = {
size?: number; size?: number;
workspace: workspace:
| AffineLegacyCloudWorkspace | AffineCloudWorkspace
| LocalWorkspace | LocalWorkspace
| AffinePublicWorkspace | AffinePublicWorkspace
| null; | null;

View File

@@ -1,5 +1,5 @@
import type { import type {
AffineLegacyCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
@@ -18,17 +18,15 @@ import { workspaceItemStyle } from './index.css';
export type WorkspaceListProps = { export type WorkspaceListProps = {
disabled?: boolean; disabled?: boolean;
currentWorkspaceId: string | null; currentWorkspaceId: string | null;
items: (AffineLegacyCloudWorkspace | LocalWorkspace)[]; items: (AffineCloudWorkspace | LocalWorkspace)[];
onClick: (workspace: AffineLegacyCloudWorkspace | LocalWorkspace) => void; onClick: (workspace: AffineCloudWorkspace | LocalWorkspace) => void;
onSettingClick: ( onSettingClick: (workspace: AffineCloudWorkspace | LocalWorkspace) => void;
workspace: AffineLegacyCloudWorkspace | LocalWorkspace
) => void;
onDragEnd: (event: DragEndEvent) => void; onDragEnd: (event: DragEndEvent) => void;
}; };
const SortableWorkspaceItem: FC< const SortableWorkspaceItem: FC<
Omit<WorkspaceListProps, 'items'> & { Omit<WorkspaceListProps, 'items'> & {
item: AffineLegacyCloudWorkspace | LocalWorkspace; item: AffineCloudWorkspace | LocalWorkspace;
} }
> = props => { > = props => {
const { setNodeRef, attributes, listeners, transform } = useSortable({ const { setNodeRef, attributes, listeners, transform } = useSortable({

View File

@@ -8,7 +8,6 @@ import type {
import type { FC, PropsWithChildren } from 'react'; import type { FC, PropsWithChildren } from 'react';
import type { Collection } from './filter'; import type { Collection } from './filter';
import type { Workspace as RemoteWorkspace } from './workspace/legacy-cloud';
export enum WorkspaceVersion { export enum WorkspaceVersion {
SubDoc = 2, SubDoc = 2,
@@ -51,18 +50,10 @@ export interface SQLiteDBDownloadProvider extends ActiveDocProvider {
flavour: 'sqlite-download'; flavour: 'sqlite-download';
} }
export interface AffineWebSocketProvider extends PassiveDocProvider {
flavour: 'affine-websocket';
}
export interface AffineLegacyCloudWorkspace extends RemoteWorkspace {
flavour: WorkspaceFlavour.AFFINE;
// empty
blockSuiteWorkspace: BlockSuiteWorkspace;
}
// todo: update type with nest.js // todo: update type with nest.js
export type AffineCloudWorkspace = LocalWorkspace; export type AffineCloudWorkspace = Omit<LocalWorkspace, 'flavour'> & {
flavour: WorkspaceFlavour.AFFINE_CLOUD;
};
export interface LocalWorkspace { export interface LocalWorkspace {
flavour: WorkspaceFlavour.LOCAL; flavour: WorkspaceFlavour.LOCAL;
@@ -89,14 +80,6 @@ export enum LoadPriority {
} }
export enum WorkspaceFlavour { export enum WorkspaceFlavour {
/**
* AFFiNE Workspace is the workspace
* that hosted on the Legacy AFFiNE Cloud Server.
*
* @deprecated
* We no longer maintain this kind of workspace, please use AFFiNE-Cloud instead.
*/
AFFINE = 'affine',
/** /**
* New AFFiNE Cloud Workspace using Nest.js Server. * New AFFiNE Cloud Workspace using Nest.js Server.
*/ */
@@ -117,10 +100,8 @@ export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
// built-in workspaces // built-in workspaces
export interface WorkspaceRegistry { export interface WorkspaceRegistry {
[WorkspaceFlavour.AFFINE]: AffineLegacyCloudWorkspace;
[WorkspaceFlavour.LOCAL]: LocalWorkspace; [WorkspaceFlavour.LOCAL]: LocalWorkspace;
[WorkspaceFlavour.PUBLIC]: AffinePublicWorkspace; [WorkspaceFlavour.PUBLIC]: AffinePublicWorkspace;
// todo: update workspace type to new
[WorkspaceFlavour.AFFINE_CLOUD]: AffineCloudWorkspace; [WorkspaceFlavour.AFFINE_CLOUD]: AffineCloudWorkspace;
} }
@@ -148,21 +129,6 @@ export type WorkspaceHeaderProps<Flavour extends keyof WorkspaceRegistry> =
}; };
}; };
type SettingProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & {
currentTab: SettingPanel;
onChangeTab: (tab: SettingPanel) => void;
onDeleteWorkspace: () => Promise<void>;
onTransformWorkspace: <
From extends keyof WorkspaceRegistry,
To extends keyof WorkspaceRegistry
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
};
type NewSettingProps<Flavour extends keyof WorkspaceRegistry> = type NewSettingProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & { UIBaseProps<Flavour> & {
onDeleteWorkspace: () => Promise<void>; onDeleteWorkspace: () => Promise<void>;
@@ -192,7 +158,6 @@ export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
Header: FC<WorkspaceHeaderProps<Flavour>>; Header: FC<WorkspaceHeaderProps<Flavour>>;
PageDetail: FC<PageDetailProps<Flavour>>; PageDetail: FC<PageDetailProps<Flavour>>;
PageList: FC<PageListProps<Flavour>>; PageList: FC<PageListProps<Flavour>>;
SettingsDetail: FC<SettingProps<Flavour>>;
NewSettingsDetail: FC<NewSettingProps<Flavour>>; NewSettingsDetail: FC<NewSettingProps<Flavour>>;
Provider: FC<PropsWithChildren>; Provider: FC<PropsWithChildren>;
} }

View File

@@ -3,16 +3,11 @@
"private": true, "private": true,
"exports": { "exports": {
"./atom": "./src/atom.ts", "./atom": "./src/atom.ts",
"./blob": "./src/blob/index.ts",
"./utils": "./src/utils.ts", "./utils": "./src/utils.ts",
"./type": "./src/type.ts", "./type": "./src/type.ts",
"./migration": "./src/migration/index.ts", "./migration": "./src/migration/index.ts",
"./local/crud": "./src/local/crud.ts", "./local/crud": "./src/local/crud.ts",
"./providers": "./src/providers/index.ts", "./providers": "./src/providers/index.ts"
"./affine/*": "./src/affine/*.ts",
"./affine/api": "./src/affine/api/index.ts",
"./affine/keck": "./src/affine/keck/index.ts",
"./affine/shared": "./src/affine/shared.ts"
}, },
"peerDependencies": { "peerDependencies": {
"@blocksuite/blocks": "*", "@blocksuite/blocks": "*",
@@ -26,7 +21,6 @@
"@toeverything/plugin-infra": "workspace:*", "@toeverything/plugin-infra": "workspace:*",
"@toeverything/y-indexeddb": "workspace:*", "@toeverything/y-indexeddb": "workspace:*",
"async-call-rpc": "^6.3.1", "async-call-rpc": "^6.3.1",
"firebase": "^9.23.0",
"jotai": "^2.2.1", "jotai": "^2.2.1",
"js-base64": "^3.7.5", "js-base64": "^3.7.5",
"ky": "^0.33.3", "ky": "^0.33.3",

View File

@@ -1,313 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { MessageCode, Messages } from '@affine/env/constant';
import type {
AcceptInvitingParams,
DeleteWorkspaceParams,
GetUserByEmailParams,
GetWorkspaceDetailParams,
InviteMemberParams,
LeaveWorkspaceParams,
Member,
Permission,
RemoveMemberParams,
UpdateWorkspaceParams,
UsageResponse,
User,
Workspace,
WorkspaceDetail,
} from '@affine/env/workspace/legacy-cloud';
import { checkLoginStorage } from '../login';
export class RequestError extends Error {
public readonly code: (typeof MessageCode)[keyof typeof MessageCode];
constructor(
code: (typeof MessageCode)[keyof typeof MessageCode],
cause: unknown | null = null
) {
super(Messages[code].message);
sendMessage(code);
this.code = code;
this.name = 'RequestError';
this.cause = cause;
}
}
function sendMessage(code: (typeof MessageCode)[keyof typeof MessageCode]) {
document.dispatchEvent(
new CustomEvent('affine-error', {
detail: {
code,
},
})
);
}
export function createUserApis(prefixUrl = '/') {
return {
getUsage: async (): Promise<UsageResponse> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + 'api/resource/usage', {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
getUserByEmail: async (
params: GetUserByEmailParams
): Promise<User[] | null> => {
const auth = await checkLoginStorage(prefixUrl);
const target = new URL(prefixUrl + 'api/user', window.location.origin);
target.searchParams.append('email', params.email);
target.searchParams.append('workspace_id', params.workspace_id);
return fetch(target, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
} as const;
}
export function createWorkspaceApis(prefixUrl = '/') {
return {
getWorkspaces: async (): Promise<Workspace[]> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + 'api/workspace', {
method: 'GET',
headers: {
Authorization: auth.token,
'Cache-Control': 'no-cache',
},
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.loadListFailed, e);
});
},
getWorkspaceDetail: async (
params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.loadListFailed, e);
});
},
getWorkspaceMembers: async (
params: GetWorkspaceDetailParams
): Promise<Member[]> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.getMembersFailed, e);
});
},
createWorkspace: async (
encodedYDoc: ArrayBuffer
): Promise<{ id: string }> => {
const auth = await checkLoginStorage();
return fetch(prefixUrl + 'api/workspace', {
method: 'POST',
body: encodedYDoc,
headers: {
'Content-Type': 'application/octet-stream',
Authorization: auth.token,
},
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.createWorkspaceFailed, e);
});
},
updateWorkspace: async (
params: UpdateWorkspaceParams
): Promise<{ public: boolean | null }> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'POST',
body: JSON.stringify({
public: params.public,
}),
headers: {
'Content-Type': 'application/json',
Authorization: auth.token,
},
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.updateWorkspaceFailed, e);
});
},
deleteWorkspace: async (
params: DeleteWorkspaceParams
): Promise<boolean> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
})
.then(r => r.ok)
.catch(e => {
throw new RequestError(MessageCode.deleteWorkspaceFailed, e);
});
},
/**
* Notice: Only support normal(contrast to private) workspace.
*/
inviteMember: async (params: InviteMemberParams): Promise<void> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
method: 'POST',
body: JSON.stringify({
email: params.email,
}),
headers: {
'Content-Type': 'application/json',
Authorization: auth.token,
},
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.inviteMemberFailed, e);
});
},
removeMember: async (params: RemoveMemberParams): Promise<void> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/permission/${params.permissionId}`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.removeMemberFailed, e);
});
},
acceptInviting: async (
params: AcceptInvitingParams
): Promise<Permission> => {
return fetch(prefixUrl + `api/invitation/${params.invitingCode}`, {
method: 'POST',
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.acceptInvitingFailed, e);
});
},
uploadBlob: async (
workspaceId: string,
arrayBuffer: ArrayBuffer,
type: string
): Promise<string> => {
const auth = await checkLoginStorage(prefixUrl);
const mb = arrayBuffer.byteLength / 1048576;
if (mb > 10) {
throw new RequestError(MessageCode.blobTooLarge);
}
return fetch(prefixUrl + `api/workspace/${workspaceId}/blob`, {
method: 'PUT',
body: arrayBuffer,
headers: {
'Content-Type': type,
Authorization: auth.token,
},
}).then(r => r.text());
},
getBlob: async (
workspaceId: string,
blobId: string
): Promise<ArrayBuffer> => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/workspace/${workspaceId}/blob/${blobId}`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
})
.then(r => r.arrayBuffer())
.catch(e => {
throw new RequestError(MessageCode.getBlobFailed, e);
});
},
leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/workspace/${id}/permission`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
})
.then(r => r.json())
.catch(e => {
throw new RequestError(MessageCode.leaveWorkspaceFailed, e);
});
},
downloadPublicWorkspacePage: async (
workspaceId: string,
pageId: string
): Promise<ArrayBuffer> => {
return fetch(
prefixUrl + `api/public/workspace/${workspaceId}/${pageId}`,
{
method: 'GET',
}
).then(r =>
r.ok
? r.arrayBuffer()
: Promise.reject(new RequestError(MessageCode.noPermission))
);
},
downloadWorkspace: async (
workspaceId: string,
published = false
): Promise<ArrayBuffer> => {
if (published) {
return fetch(prefixUrl + `api/public/workspace/${workspaceId}`, {
method: 'GET',
}).then(r => r.arrayBuffer());
} else {
const auth = await checkLoginStorage(prefixUrl);
return fetch(prefixUrl + `api/workspace/${workspaceId}/doc`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
})
.then(r =>
!r.ok
? Promise.reject(new RequestError(MessageCode.noPermission))
: r
)
.then(r => r.arrayBuffer())
.catch(e => {
if (e instanceof RequestError) {
throw e;
}
throw new RequestError(MessageCode.downloadWorkspaceFailed, e);
});
}
},
} as const;
}

View File

@@ -1,10 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
export function createStatusApis(prefixUrl = '/') {
return {
healthz: async (): Promise<boolean> => {
return fetch(`${prefixUrl}api/healthz`).then(r => r.status === 204);
},
} as const;
}

View File

@@ -1,11 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { atomWithStorage } from 'jotai/utils';
import type { AccessTokenMessage } from '../affine/login';
export const currentAffineUserAtom = atomWithStorage<AccessTokenMessage | null>(
'affine-user-atom',
null
);

View File

@@ -1,99 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { assertExists } from '@blocksuite/global/utils';
import * as url from 'lib0/url';
import * as websocket from 'lib0/websocket';
import { getLoginStorage, isExpired, parseIdToken } from '../affine/login';
import { cleanupWorkspace } from '../utils';
const RECONNECT_INTERVAL_TIME = 500;
const MAX_RECONNECT_TIMES = 50;
export class WebsocketClient {
public readonly baseServerUrl: string;
private _client: websocket.WebsocketClient | null = null;
public shouldReconnect = false;
private _retryTimes = 0;
private _logger = new DebugLogger('affine:channel');
private _callback: ((message: any) => void) | null = null;
constructor(serverUrl: string) {
while (serverUrl.endsWith('/')) {
serverUrl = serverUrl.slice(0, serverUrl.length - 1);
}
this.baseServerUrl = serverUrl;
}
public connect(callback: (message: any) => void) {
const loginResponse = getLoginStorage();
if (!loginResponse || isExpired(parseIdToken(loginResponse.token))) {
cleanupWorkspace(WorkspaceFlavour.AFFINE);
return;
}
assertExists(loginResponse, 'loginResponse is null');
const encodedParams = url.encodeQueryParams({
token: loginResponse.token,
});
const serverUrl =
this.baseServerUrl +
(encodedParams.length === 0 ? '' : '?' + encodedParams);
this._client = new websocket.WebsocketClient(serverUrl);
this._callback = callback;
this._setupChannel();
this._client.on('message', this._callback);
}
public disconnect() {
assertExists(this._client, 'client is null');
if (this._callback) {
this._client.off('message', this._callback);
}
this._client.disconnect();
this._client.destroy();
this._client = null;
}
private _setupChannel() {
assertExists(this._client, 'client is null');
const client = this._client;
client.on('connect', () => {
this._logger.debug('Affine channel connected');
this.shouldReconnect = true;
this._retryTimes = 0;
});
client.on('disconnect', ({ error }: { error: Error }) => {
if (error) {
const loginResponse = getLoginStorage();
const isLogin = loginResponse
? isExpired(parseIdToken(loginResponse.token))
: false;
// Try to re-connect if connect error has occurred
if (this.shouldReconnect && isLogin && !client.connected) {
try {
setTimeout(() => {
if (this._retryTimes <= MAX_RECONNECT_TIMES) {
assertExists(this._callback, 'callback is null');
this.connect(this._callback);
this._logger.info(
`try reconnect channel ${++this._retryTimes} times`
);
} else {
this._logger.error(
'reconnect failed, max reconnect times reached'
);
}
}, RECONNECT_INTERVAL_TIME);
} catch (e) {
this._logger.error('reconnect failed', e);
}
}
}
});
}
}

View File

@@ -1,5 +0,0 @@
# Keck
> This directory will be removed in the future once we publish the jwt library to npm.
The latest Keck code of AFFiNE is at https://github.com/toeverything/OctoBase/tree/master/libs/jwt

View File

@@ -1,59 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import * as decoding from 'lib0/decoding';
import * as encoding from 'lib0/encoding';
import * as awarenessProtocol from 'y-protocols/awareness';
import * as syncProtocol from 'y-protocols/sync';
import type { KeckProvider } from '.';
export enum Message {
sync = 0,
awareness = 1,
queryAwareness = 3,
}
export type MessageCallback = (
encoder: encoding.Encoder,
decoder: decoding.Decoder,
provider: KeckProvider,
emitSynced: boolean,
messageType: number
) => void;
export const handler: Record<Message, MessageCallback> = {
[Message.sync]: (encoder, decoder, provider, emitSynced) => {
encoding.writeVarUint(encoder, Message.sync);
const syncMessageType = syncProtocol.readSyncMessage(
decoder,
encoder,
provider.doc,
provider
);
if (
emitSynced &&
syncMessageType === syncProtocol.messageYjsSyncStep2 &&
!provider.synced
) {
provider.synced = true;
}
},
[Message.awareness]: (_encoder, decoder, provider) => {
awarenessProtocol.applyAwarenessUpdate(
provider.awareness,
decoding.readVarUint8Array(decoder),
provider
);
},
[Message.queryAwareness]: (encoder, _decoder, provider) => {
encoding.writeVarUint(encoder, Message.awareness);
encoding.writeVarUint8Array(
encoder,
awarenessProtocol.encodeAwarenessUpdate(
provider.awareness,
Array.from(provider.awareness.getStates().keys())
)
);
},
};

View File

@@ -1,290 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { isBrowser } from '@affine/env/constant';
import * as encoding from 'lib0/encoding';
import * as math from 'lib0/math';
import { Observable } from 'lib0/observable';
import * as url from 'lib0/url';
import * as awarenessProtocol from 'y-protocols/awareness';
import * as syncProtocol from 'y-protocols/sync';
import type * as Y from 'yjs';
import { handler, Message } from './handler';
import { readMessage } from './processor';
// @todo - this should depend on awareness.outdatedTime
const messageReconnectTimeout = 30000;
const setupWS = (provider: KeckProvider) => {
if (provider.shouldConnect && provider.ws === null) {
const websocket = new WebSocket(provider.url);
websocket.binaryType = 'arraybuffer';
provider.ws = websocket;
provider.wsconnecting = true;
provider.wsconnected = false;
provider.synced = false;
websocket.onmessage = (event: any) => {
provider.wsLastMessageReceived = Date.now();
const encoder = readMessage(provider, new Uint8Array(event.data), true);
if (encoding.length(encoder) > 1) {
websocket.send(encoding.toUint8Array(encoder));
}
};
websocket.onerror = (event: any) => {
provider.emit('connection-error', [event, provider]);
};
websocket.onclose = (event: any) => {
provider.emit('connection-close', [event, provider]);
provider.ws = null;
provider.wsconnecting = false;
if (provider.wsconnected) {
provider.wsconnected = false;
provider.synced = false;
// update awareness (all users except local left)
awarenessProtocol.removeAwarenessStates(
provider.awareness,
Array.from(provider.awareness.getStates().keys()).filter(
client => client !== provider.doc.clientID
),
provider
);
provider.emit('status', [
{
status: 'disconnected',
},
]);
} else {
provider.wsUnsuccessfulReconnects++;
}
// Start with no reconnect timeout and increase timeout by
// using exponential backoff starting with 100ms
setTimeout(
setupWS,
math.min(
math.pow(2, provider.wsUnsuccessfulReconnects) * 100,
provider.maxBackOffTime
) + provider.extraToleranceTime,
provider
);
};
websocket.onopen = () => {
provider.wsLastMessageReceived = Date.now();
provider.wsconnecting = false;
provider.wsconnected = true;
provider.wsUnsuccessfulReconnects = 0;
provider.emit('status', [
{
status: 'connected',
},
]);
// always send sync step 1 when connected
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, Message.sync);
syncProtocol.writeSyncStep1(encoder, provider.doc);
websocket.send(encoding.toUint8Array(encoder));
// broadcast local awareness state
if (provider.awareness.getLocalState() !== null) {
const encoderAwarenessState = encoding.createEncoder();
encoding.writeVarUint(encoderAwarenessState, Message.awareness);
encoding.writeVarUint8Array(
encoderAwarenessState,
awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [
provider.doc.clientID,
])
);
websocket.send(encoding.toUint8Array(encoderAwarenessState));
}
};
provider.emit('status', [
{
status: 'connecting',
},
]);
}
};
const broadcastMessage = (provider: KeckProvider, buf: ArrayBuffer) => {
const ws = provider.ws;
if (provider.wsconnected && ws && ws.readyState === ws.OPEN) {
ws.send(buf);
}
};
export class KeckProvider extends Observable<string> {
doc: Y.Doc;
awareness: awarenessProtocol.Awareness;
url: any;
messageHandlers: typeof handler;
shouldConnect: boolean;
ws: any;
wsconnecting: boolean;
wsconnected: boolean;
wsLastMessageReceived: number;
wsUnsuccessfulReconnects: any;
maxBackOffTime: number;
roomName: string;
_synced: boolean;
_resyncInterval: any;
extraToleranceTime: number;
_updateHandler: (update: Uint8Array, origin: any) => void;
_awarenessUpdateHandler: ({ added, updated, removed }: any) => void;
_unloadHandler: () => void;
_checkInterval: NodeJS.Timer;
constructor(
serverUrl: string,
roomName: string,
doc: Y.Doc,
{
connect = true,
awareness = new awarenessProtocol.Awareness(doc),
params = {},
resyncInterval = -1,
maxBackOffTime = 2500,
extraToleranceTime = 0,
} = {}
) {
super();
// ensure that url is always ends with /
while (serverUrl[serverUrl.length - 1] === '/') {
serverUrl = serverUrl.slice(0, serverUrl.length - 1);
}
const encodedParams = url.encodeQueryParams(params);
this.maxBackOffTime = maxBackOffTime;
this.extraToleranceTime = extraToleranceTime;
this.url =
serverUrl +
'/' +
roomName +
(encodedParams.length === 0 ? '' : '?' + encodedParams);
this.roomName = roomName;
this.doc = doc;
this.awareness = awareness;
this.wsconnected = false;
this.wsconnecting = false;
this.wsUnsuccessfulReconnects = 0;
this.messageHandlers = handler;
/**
* @type {boolean}
*/
this._synced = false;
/**
* @type {WebSocket?}
*/
this.ws = null;
this.wsLastMessageReceived = 0;
/**
* Whether to connect to other peers or not
* @type {boolean}
*/
this.shouldConnect = connect;
this._resyncInterval = 0;
if (resyncInterval > 0) {
this._resyncInterval = /** @type {any} */ setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
// resend sync step 1
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, Message.sync);
syncProtocol.writeSyncStep1(encoder, doc);
this.ws.send(encoding.toUint8Array(encoder));
}
}, resyncInterval);
}
this._updateHandler = (update: Uint8Array, origin: any) => {
if (origin !== this) {
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, Message.sync);
syncProtocol.writeUpdate(encoder, update);
broadcastMessage(this, encoding.toUint8Array(encoder));
}
};
this.doc.on('update', this._updateHandler);
this._awarenessUpdateHandler = ({ added, updated, removed }: any) => {
const changedClients = added.concat(updated).concat(removed);
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, Message.awareness);
encoding.writeVarUint8Array(
encoder,
awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)
);
broadcastMessage(this, encoding.toUint8Array(encoder));
};
this._unloadHandler = () => {
awarenessProtocol.removeAwarenessStates(
this.awareness,
[doc.clientID],
'window unload'
);
};
if (isBrowser) {
window.addEventListener('unload', this._unloadHandler);
} else if (typeof process !== 'undefined') {
process.on('exit', this._unloadHandler);
}
awareness.on('update', this._awarenessUpdateHandler);
this._checkInterval = /** @type {any} */ setInterval(() => {
if (
this.wsconnected &&
messageReconnectTimeout < Date.now() - this.wsLastMessageReceived
) {
// no message received in a long time - not even your own awareness
// updates (which are updated every 15 seconds)
/** @type {WebSocket} */ this.ws.close();
}
}, messageReconnectTimeout / 10);
if (connect) {
this.connect();
}
}
/**
* @type {boolean}
*/
get synced() {
return this._synced;
}
set synced(state) {
if (this._synced !== state) {
this._synced = state;
this.emit('synced', [state]);
this.emit('sync', [state]);
}
}
override destroy() {
if (this._resyncInterval !== 0) {
clearInterval(this._resyncInterval);
}
clearInterval(this._checkInterval);
this.disconnect();
if (isBrowser) {
window.removeEventListener('unload', this._unloadHandler);
} else if (typeof process !== 'undefined') {
process.off('exit', this._unloadHandler);
}
this.awareness.off('update', this._awarenessUpdateHandler);
this.doc.off('update', this._updateHandler);
super.destroy();
}
disconnect() {
this.shouldConnect = false;
if (this.ws !== null) {
this.ws.close();
}
}
connect() {
this.shouldConnect = true;
if (!this.wsconnected && this.ws === null) {
setupWS(this);
}
}
}

View File

@@ -1,25 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import * as decoding from 'lib0/decoding';
import * as encoding from 'lib0/encoding';
import type { KeckProvider } from '.';
import type { Message } from './handler.js';
export const readMessage = (
provider: KeckProvider,
buf: Uint8Array,
emitSynced: boolean
): encoding.Encoder => {
const decoder = decoding.createDecoder(buf);
const encoder = encoding.createEncoder();
const messageType = decoding.readVarUint(decoder) as Message;
const messageHandler = provider.messageHandlers[messageType];
if (messageHandler) {
messageHandler(encoder, decoder, provider, emitSynced, messageType);
} else {
console.error('Unable to compute message');
}
return encoder;
};

View File

@@ -1,238 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { DebugLogger } from '@affine/debug';
import { assertExists } from '@blocksuite/global/utils';
import { Slot } from '@blocksuite/store';
import { initializeApp } from 'firebase/app';
import type { AuthProvider } from 'firebase/auth';
import {
type Auth as FirebaseAuth,
connectAuthEmulator,
getAuth as getFirebaseAuth,
GithubAuthProvider,
GoogleAuthProvider,
signInWithCredential,
signInWithPopup,
} from 'firebase/auth';
import { decode } from 'js-base64';
import { z } from 'zod';
// Connect emulators based on env vars
const envConnectEmulators = process.env.REACT_APP_FIREBASE_EMULATORS === 'true';
export type AccessTokenMessage = {
created_at: number;
exp: number;
email: string;
id: string;
name: string;
avatar_url: string;
};
export type LoginParams = {
type: 'Google' | 'Refresh';
token: string;
};
export const loginResponseSchema = z.object({
token: z.string(),
refresh: z.string(),
});
export type LoginResponse = z.infer<typeof loginResponseSchema>;
const logger = new DebugLogger('token');
export const STORAGE_KEY = 'affine-login-v2';
export function parseIdToken(token: string): AccessTokenMessage {
return JSON.parse(decode(token.split('.')[1]));
}
export const isExpired = (
token: AccessTokenMessage,
// earlier than `before`, consider it expired
before = 60 // 1 minute
): boolean => {
const now = Math.floor(Date.now() / 1000);
return token.exp < now - before;
};
export const setLoginStorage = (login: LoginResponse) => {
loginResponseSchema.parse(login);
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
token: login.token,
refresh: login.refresh,
})
);
};
const signInWithElectron = async (firebaseAuth: FirebaseAuth) => {
if (window.apis) {
const { url, requestInit } = await window.apis.ui.getGoogleOauthCode();
const { id_token } = await fetch(url, requestInit).then(res => res.json());
const credential = GoogleAuthProvider.credential(id_token);
const user = await signInWithCredential(firebaseAuth, credential);
return await user.user.getIdToken();
}
return void 0;
};
export const clearLoginStorage = () => {
localStorage.removeItem(STORAGE_KEY);
};
export const getLoginStorage = (): LoginResponse | null => {
const login = localStorage.getItem(STORAGE_KEY);
if (login) {
try {
return JSON.parse(login);
} catch (error) {
logger.error('Failed to parse login', error);
}
}
return null;
};
export const storageChangeSlot = new Slot();
export const checkLoginStorage = async (
prefixUrl = '/'
): Promise<LoginResponse> => {
const storage = getLoginStorage();
assertExists(storage, 'Login token is not set');
if (isExpired(parseIdToken(storage.token), 0)) {
logger.debug('refresh token needed');
const response: LoginResponse = await fetch(prefixUrl + 'api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'Refresh',
token: storage.refresh,
}),
}).then(r => r.json());
setLoginStorage(response);
logger.debug('refresh token emit');
storageChangeSlot.emit();
}
return getLoginStorage() as LoginResponse;
};
export enum SignMethod {
Google = 'Google',
GitHub = 'GitHub',
// Twitter = 'Twitter',
}
declare global {
// eslint-disable-next-line no-var
var firebaseAuthEmulatorStarted: boolean | undefined;
}
export function createAffineAuth(prefix = '/') {
let _firebaseAuth: FirebaseAuth | null = null;
const getAuth = (): FirebaseAuth | null => {
try {
if (!_firebaseAuth) {
const app = initializeApp({
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId:
process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
});
_firebaseAuth = getFirebaseAuth(app);
}
if (envConnectEmulators && !globalThis.firebaseAuthEmulatorStarted) {
connectAuthEmulator(_firebaseAuth, 'http://localhost:9099', {
disableWarnings: true,
});
globalThis.firebaseAuthEmulatorStarted = true;
}
return _firebaseAuth;
} catch (error) {
logger.error('Failed to initialize firebase', error);
return null;
}
};
return {
generateToken: async (
method: SignMethod
): Promise<LoginResponse | null> => {
const auth = getAuth();
if (!auth) {
throw new Error('Failed to initialize firebase');
}
let provider: AuthProvider;
switch (method) {
case SignMethod.Google: {
const googleProvider = new GoogleAuthProvider();
// make sure the user has a chance to select an account
// https://developers.google.com/identity/openid-connect/openid-connect#prompt
googleProvider.setCustomParameters({
prompt: 'select_account',
});
provider = googleProvider;
break;
}
case SignMethod.GitHub:
provider = new GithubAuthProvider();
break;
default:
throw new Error('Unsupported sign method');
}
try {
let idToken: string | undefined;
if (environment.isDesktop) {
idToken = await signInWithElectron(auth);
} else {
const response = await signInWithPopup(auth, provider);
idToken = await response.user.getIdToken();
}
logger.debug('idToken', idToken);
return fetch(prefix + 'api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'Google',
token: idToken,
}),
}).then(r => r.json()) as Promise<LoginResponse>;
} catch (error) {
if (
error instanceof Error &&
'code' in error &&
error.code === 'auth/popup-closed-by-user'
) {
return null;
}
logger.error('Failed to sign in', error);
}
return null;
},
refreshToken: async (
loginResponse: LoginResponse
): Promise<LoginResponse | null> => {
return fetch(prefix + 'api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'Refresh',
token: loginResponse.refresh,
}),
}).then(r => r.json()) as Promise<LoginResponse>;
},
} as const;
}

View File

@@ -1,76 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { setupGlobal } from '@affine/env/global';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { createUserApis, createWorkspaceApis } from './api/index';
import { currentAffineUserAtom } from './atom';
import type { LoginResponse } from './login';
import { createAffineAuth, parseIdToken, setLoginStorage } from './login';
setupGlobal();
export const affineAuth = createAffineAuth(prefixUrl);
const affineApis = {} as ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>;
Object.assign(affineApis, createUserApis(prefixUrl));
Object.assign(affineApis, createWorkspaceApis(prefixUrl));
if (!globalThis.AFFINE_APIS) {
globalThis.AFFINE_APIS = affineApis;
globalThis.setLogin = (response: LoginResponse) => {
rootStore.set(currentAffineUserAtom, parseIdToken(response.token));
setLoginStorage(response);
};
const loginMockUser1 = async () => {
const user1 = await import('@affine-test/fixtures/built-in-user1.json');
const data = await fetch(prefixUrl + 'api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'DebugLoginUser',
email: user1.email,
password: user1.password,
}),
}).then(r => r.json());
setLogin(data);
};
const loginMockUser2 = async () => {
const user2 = await import('@affine-test/fixtures/built-in-user2.json');
const data = await fetch(prefixUrl + 'api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'DebugLoginUser',
email: user2.email,
password: user2.password,
}),
}).then(r => r.json());
setLogin(data);
};
globalThis.AFFINE_DEBUG = {
loginMockUser1,
loginMockUser2,
};
}
declare global {
// eslint-disable-next-line no-var
var setLogin: typeof setLoginStorage;
// eslint-disable-next-line no-var
var AFFINE_APIS:
| undefined
| (ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>);
// eslint-disable-next-line no-var
var AFFINE_DEBUG: Record<string, unknown>;
}
export { affineApis };

View File

@@ -1,85 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { DebugLogger } from '@affine/debug';
import type { WorkspaceCRUD } from '@affine/env/workspace';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import {
workspaceDetailSchema,
workspaceSchema,
} from '@affine/env/workspace/legacy-cloud';
import { assertExists } from '@blocksuite/global/utils';
import type { Disposable } from '@blocksuite/store';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { z } from 'zod';
import { WebsocketClient } from '../affine/channel';
import { storageChangeSlot } from '../affine/login';
import { rootWorkspacesMetadataAtom } from '../atom';
const logger = new DebugLogger('affine-sync');
const channelMessageSchema = z.object({
ws_list: z.array(workspaceSchema),
ws_details: z.record(workspaceDetailSchema),
metadata: z.record(
z.object({
search_index: z.array(z.string()),
name: z.string(),
})
),
});
type ChannelMessage = z.infer<typeof channelMessageSchema>;
export function createAffineGlobalChannel(
crud: WorkspaceCRUD<WorkspaceFlavour.AFFINE>
) {
let client: WebsocketClient | null;
async function handleMessage(channelMessage: ChannelMessage) {
logger.debug('channelMessage', channelMessage);
const parseResult = channelMessageSchema.safeParse(channelMessage);
if (!parseResult.success) {
console.error(
'channelMessageSchema.safeParse(channelMessage) failed',
parseResult
);
}
const { ws_details } = channelMessage;
const currentWorkspaces = await crud.list();
for (const [id] of Object.entries(ws_details)) {
const workspaceIndex = currentWorkspaces.findIndex(
workspace => workspace.id === id
);
// If the workspace is not in the current workspace list, remove it
if (workspaceIndex === -1) {
await rootStore.set(rootWorkspacesMetadataAtom, workspaces => {
const idx = workspaces.findIndex(workspace => workspace.id === id);
workspaces.splice(idx, 1);
return [...workspaces];
});
}
}
}
let dispose: Disposable | undefined = undefined;
const apis = {
connect: () => {
client = new WebsocketClient(websocketPrefixUrl + '/api/global/sync/');
client.connect(handleMessage);
dispose = storageChangeSlot.on(() => {
apis.disconnect();
apis.connect();
});
},
disconnect: () => {
assertExists(client, 'client is null');
client.disconnect();
dispose?.dispose();
client = null;
},
};
return apis;
}

View File

@@ -1,118 +0,0 @@
import { DebugLogger } from '@affine/debug';
import type { BlobStorage } from '@blocksuite/store';
import { createIndexeddbStorage } from '@blocksuite/store';
import { openDB } from 'idb';
import type { DBSchema } from 'idb/build/entry';
import type { createWorkspaceApis } from '../affine/api';
type UploadingBlob = {
key: string;
arrayBuffer: ArrayBuffer;
type: string;
};
interface AffineBlob extends DBSchema {
uploading: {
key: string;
value: UploadingBlob;
};
// todo: migrate blob storage from `createIndexeddbStorage`
}
const logger = new DebugLogger('affine:blob');
export const createAffineBlobStorage = (
workspaceId: string,
workspaceApis: ReturnType<typeof createWorkspaceApis>
): BlobStorage => {
const storage = createIndexeddbStorage(workspaceId);
const dbPromise = openDB<AffineBlob>('affine-blob', 1, {
upgrade(db) {
db.createObjectStore('uploading', { keyPath: 'key' });
},
});
dbPromise
.then(async db => {
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
await t.getAll().then(blobs =>
blobs.map(({ arrayBuffer, type }) =>
workspaceApis.uploadBlob(workspaceId, arrayBuffer, type).then(key => {
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
return t.delete(key);
})
)
);
})
.catch(err => {
logger.error('[createAffineBlobStorage] dbPromise error', err);
});
return {
crud: {
get: async key => {
const blob = await storage.crud.get(key);
if (!blob) {
const buffer = await workspaceApis.getBlob(workspaceId, key);
return new Blob([buffer]);
} else {
return blob;
}
},
set: async (key, value) => {
const db = await dbPromise;
const arrayBuffer = await value.arrayBuffer();
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
let uploaded = false;
await t.put({
key,
arrayBuffer,
type: value.type,
});
// delete the uploading blob after uploaded
if (uploaded) {
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
// don't await here, we don't care if it's deleted
t.delete(key).catch(err => {
logger.error('[createAffineBlobStorage] delete error', err);
});
}
await Promise.all([
storage.crud.set(key, value),
workspaceApis
.uploadBlob(workspaceId, await value.arrayBuffer(), value.type)
.then(async () => {
uploaded = true;
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
// delete the uploading blob after uploaded
if (await t.get(key)) {
await t.delete(key);
}
}),
]);
return key;
},
delete: async (key: string) => {
await Promise.all([
storage.crud.delete(key),
// we don't support deleting a blob in API?
// workspaceApis.deleteBlob(workspaceId, key)
]);
},
list: async () => {
const blobs = await storage.crud.list();
// we don't support listing blobs in API?
return [...blobs];
},
},
};
};

View File

@@ -1,57 +0,0 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { DebugLogger } from '@affine/debug';
import type { AffineDownloadProvider } from '@affine/env/workspace';
import type { DocProviderCreator } from '@blocksuite/store';
import { Workspace } from '@blocksuite/store';
import { affineApis } from '../affine/shared';
const hashMap = new Map<string, ArrayBuffer>();
const logger = new DebugLogger('affine:workspace:download-provider');
export const createAffineDownloadProvider: DocProviderCreator = (
id,
doc
): AffineDownloadProvider => {
let connected = false;
return {
flavour: 'affine-download',
passive: true,
get connected() {
return connected;
},
connect: () => {
logger.info('connect download provider', id);
if (hashMap.has(id)) {
logger.debug('applyUpdate');
Workspace.Y.applyUpdate(
doc,
new Uint8Array(hashMap.get(id) as ArrayBuffer)
);
connected = true;
return;
}
affineApis
.downloadWorkspace(id, false)
.then(binary => {
hashMap.set(id, binary);
logger.debug('applyUpdate');
Workspace.Y.applyUpdate(doc, new Uint8Array(binary));
connected = true;
})
.catch(e => {
logger.error('downloadWorkspace', e);
});
},
disconnect: () => {
logger.info('disconnect download provider', id);
connected = false;
},
cleanup: () => {
hashMap.delete(id);
},
};
};

View File

@@ -1,9 +1,8 @@
import type { import type {
AffineWebSocketProvider,
LocalIndexedDBBackgroundProvider, LocalIndexedDBBackgroundProvider,
LocalIndexedDBDownloadProvider, LocalIndexedDBDownloadProvider,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import type { Disposable, DocProviderCreator } from '@blocksuite/store'; import type { DocProviderCreator } from '@blocksuite/store';
import { assertExists, Workspace } from '@blocksuite/store'; import { assertExists, Workspace } from '@blocksuite/store';
import { createBroadcastChannelProvider } from '@blocksuite/store/providers/broadcast-channel'; import { createBroadcastChannelProvider } from '@blocksuite/store/providers/broadcast-channel';
import { import {
@@ -13,10 +12,7 @@ import {
} from '@toeverything/y-indexeddb'; } from '@toeverything/y-indexeddb';
import type { Doc } from 'yjs'; import type { Doc } from 'yjs';
import { KeckProvider } from '../affine/keck';
import { getLoginStorage, storageChangeSlot } from '../affine/login';
import { CallbackSet } from '../utils'; import { CallbackSet } from '../utils';
import { createAffineDownloadProvider } from './affine-download';
import { localProviderLogger as logger } from './logger'; import { localProviderLogger as logger } from './logger';
import { import {
createSQLiteDBDownloadProvider, createSQLiteDBDownloadProvider,
@@ -25,59 +21,6 @@ import {
const Y = Workspace.Y; const Y = Workspace.Y;
const createAffineWebSocketProvider: DocProviderCreator = (
id,
doc,
{ awareness }
): AffineWebSocketProvider => {
let webSocketProvider: KeckProvider | null = null;
let dispose: Disposable | undefined = undefined;
const callbacks = new CallbackSet();
const cb = () => callbacks.forEach(cb => cb());
const apis = {
flavour: 'affine-websocket',
passive: true,
get connected() {
return callbacks.ready;
},
cleanup: () => {
assertExists(webSocketProvider);
webSocketProvider.destroy();
webSocketProvider = null;
dispose?.dispose();
},
connect: () => {
dispose = storageChangeSlot.on(() => {
apis.disconnect();
apis.connect();
});
webSocketProvider = new KeckProvider(
websocketPrefixUrl + '/api/sync/',
id,
doc,
{
params: { token: getLoginStorage()?.token ?? '' },
awareness,
// we maintain a broadcast channel by ourselves
connect: false,
}
);
logger.info('connect', webSocketProvider.url);
webSocketProvider.on('synced', cb);
webSocketProvider.connect();
},
disconnect: () => {
assertExists(webSocketProvider);
logger.info('disconnect', webSocketProvider.url);
webSocketProvider.disconnect();
webSocketProvider.off('synced', cb);
dispose?.dispose();
},
} satisfies AffineWebSocketProvider;
return apis;
};
const createIndexedDBBackgroundProvider: DocProviderCreator = ( const createIndexedDBBackgroundProvider: DocProviderCreator = (
id, id,
blockSuiteWorkspace blockSuiteWorkspace
@@ -153,8 +96,6 @@ const createIndexedDBDownloadProvider: DocProviderCreator = (
}; };
export { export {
createAffineDownloadProvider,
createAffineWebSocketProvider,
createBroadcastChannelProvider, createBroadcastChannelProvider,
createIndexedDBBackgroundProvider, createIndexedDBBackgroundProvider,
createIndexedDBDownloadProvider, createIndexedDBDownloadProvider,
@@ -182,8 +123,6 @@ export const createLocalProviders = (): DocProviderCreator[] => {
export const createAffineProviders = (): DocProviderCreator[] => { export const createAffineProviders = (): DocProviderCreator[] => {
return ( return (
[ [
createAffineDownloadProvider,
createAffineWebSocketProvider,
runtimeConfig.enableBroadcastChannelProvider && runtimeConfig.enableBroadcastChannelProvider &&
createBroadcastChannelProvider, createBroadcastChannelProvider,
createIndexedDBDownloadProvider, createIndexedDBDownloadProvider,

View File

@@ -14,9 +14,7 @@ import type {
import { createIndexeddbStorage, Workspace } from '@blocksuite/store'; import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
import { rootStore } from '@toeverything/plugin-infra/manager'; import { rootStore } from '@toeverything/plugin-infra/manager';
import type { createWorkspaceApis } from './affine/api';
import { rootWorkspacesMetadataAtom } from './atom'; import { rootWorkspacesMetadataAtom } from './atom';
import { createAffineBlobStorage } from './blob';
import { createSQLiteStorage } from './blob/sqlite-blob-storage'; import { createSQLiteStorage } from './blob/sqlite-blob-storage';
export function cleanupWorkspace(flavour: WorkspaceFlavour) { export function cleanupWorkspace(flavour: WorkspaceFlavour) {
@@ -49,9 +47,8 @@ export const _cleanupBlockSuiteWorkspaceCache = () => hashMap.clear();
export function createEmptyBlockSuiteWorkspace( export function createEmptyBlockSuiteWorkspace(
id: string, id: string,
flavour: WorkspaceFlavour.AFFINE, flavour: WorkspaceFlavour.AFFINE_CLOUD,
config: { config: {
workspaceApis: ReturnType<typeof createWorkspaceApis>;
cachePrefix?: string; cachePrefix?: string;
idGenerator?: Generator; idGenerator?: Generator;
} }
@@ -60,7 +57,6 @@ export function createEmptyBlockSuiteWorkspace(
id: string, id: string,
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
config?: { config?: {
workspaceApis?: ReturnType<typeof createWorkspaceApis>;
cachePrefix?: string; cachePrefix?: string;
idGenerator?: Generator; idGenerator?: Generator;
} }
@@ -69,18 +65,10 @@ export function createEmptyBlockSuiteWorkspace(
id: string, id: string,
flavour: WorkspaceFlavour, flavour: WorkspaceFlavour,
config?: { config?: {
workspaceApis?: ReturnType<typeof createWorkspaceApis>;
cachePrefix?: string; cachePrefix?: string;
idGenerator?: Generator; idGenerator?: Generator;
} }
): Workspace { ): Workspace {
if (
flavour === WorkspaceFlavour.AFFINE &&
!config?.workspaceApis?.getBlob &&
!config?.workspaceApis?.uploadBlob
) {
throw new Error('workspaceApis is required for affine flavour');
}
const providerCreators: DocProviderCreator[] = []; const providerCreators: DocProviderCreator[] = [];
const prefix: string = config?.cachePrefix ?? ''; const prefix: string = config?.cachePrefix ?? '';
const cacheKey = `${prefix}${id}`; const cacheKey = `${prefix}${id}`;
@@ -91,10 +79,14 @@ export function createEmptyBlockSuiteWorkspace(
const blobStorages: StoreOptions['blobStorages'] = []; const blobStorages: StoreOptions['blobStorages'] = [];
if (flavour === WorkspaceFlavour.AFFINE) { if (flavour === WorkspaceFlavour.AFFINE_CLOUD) {
if (config && config.workspaceApis) { if (isBrowser) {
const workspaceApis = config.workspaceApis; blobStorages.push(createIndexeddbStorage);
blobStorages.push(id => createAffineBlobStorage(id, workspaceApis)); if (isDesktop && runtimeConfig.enableSQLiteProvider) {
blobStorages.push(createSQLiteStorage);
}
// todo: add support for cloud storage
} }
providerCreators.push(...createAffineProviders()); providerCreators.push(...createAffineProviders());
} else { } else {

682
yarn.lock
View File

@@ -595,7 +595,6 @@ __metadata:
"@toeverything/y-indexeddb": "workspace:*" "@toeverything/y-indexeddb": "workspace:*"
"@types/ws": ^8.5.5 "@types/ws": ^8.5.5
async-call-rpc: ^6.3.1 async-call-rpc: ^6.3.1
firebase: ^9.23.0
jotai: ^2.2.1 jotai: ^2.2.1
js-base64: ^3.7.5 js-base64: ^3.7.5
ky: ^0.33.3 ky: ^0.33.3
@@ -5002,513 +5001,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@firebase/analytics-compat@npm:0.2.6":
version: 0.2.6
resolution: "@firebase/analytics-compat@npm:0.2.6"
dependencies:
"@firebase/analytics": 0.10.0
"@firebase/analytics-types": 0.8.0
"@firebase/component": 0.6.4
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: 6ec53ad2778d379aa98e3013995f65adb87bb8251b329f4dcc27b8bbefaf271539bcd46b895c8158cfd2dbea6d105c3fad9db637b9d8a81a24522e8df27b7976
languageName: node
linkType: hard
"@firebase/analytics-types@npm:0.8.0":
version: 0.8.0
resolution: "@firebase/analytics-types@npm:0.8.0"
checksum: fe8647ccf22e1cf49268c70a52f6adbaffaf4067f545fbd32b0f8d3da4a02c9889c3cc300ea289facd2db8ccb5852336a951838f746e76e8bfd1e3f68d65c63d
languageName: node
linkType: hard
"@firebase/analytics@npm:0.10.0":
version: 0.10.0
resolution: "@firebase/analytics@npm:0.10.0"
dependencies:
"@firebase/component": 0.6.4
"@firebase/installations": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: b234481de161da3f85cef04c46d0a38fb495dc3e9bb052b960b1ad73a0e2e5294acbe92db4727d56832512bbf140149370b9c616ed12c43f238c7fd9817a412a
languageName: node
linkType: hard
"@firebase/app-check-compat@npm:0.3.7":
version: 0.3.7
resolution: "@firebase/app-check-compat@npm:0.3.7"
dependencies:
"@firebase/app-check": 0.8.0
"@firebase/app-check-types": 0.5.0
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: 127af631b381eecd0c315d09070e13d8ea37923388a720c6081a170c48df4ba12faaf9b2e0e739024fc398816da0f417c1e8157560e4b8f6b10409edca8f795e
languageName: node
linkType: hard
"@firebase/app-check-interop-types@npm:0.3.0":
version: 0.3.0
resolution: "@firebase/app-check-interop-types@npm:0.3.0"
checksum: e8b6adfe47ea4149e7a330890ee2feca47d9c48323dd9a1a2247b63879c89fe5e8869c93ec36927639e1d7951a5b365623032f66ef8086981cf08f9504b18c2b
languageName: node
linkType: hard
"@firebase/app-check-types@npm:0.5.0":
version: 0.5.0
resolution: "@firebase/app-check-types@npm:0.5.0"
checksum: 39828d64e31ece1b7c38936bc4b83317c4d1f72e6c261ae1b7f6fb0f862c4ca7c84bc090ba1f2d4c815b7a2516a7f828dcbbccfe48584c7c7e8f3248ba0071ce
languageName: node
linkType: hard
"@firebase/app-check@npm:0.8.0":
version: 0.8.0
resolution: "@firebase/app-check@npm:0.8.0"
dependencies:
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: 1e0c344be6076c223dc0cc178af7b72cb37d98c4e75077d5ab8fb59dd78c3b7881f206415bc295af12dd604fd5424017a2cdce94db1a70f334fdeb83412b8978
languageName: node
linkType: hard
"@firebase/app-compat@npm:0.2.13":
version: 0.2.13
resolution: "@firebase/app-compat@npm:0.2.13"
dependencies:
"@firebase/app": 0.9.13
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
checksum: c79a1b5a6acf0d8e09e579dffc82ff1c526dcbc28f18277162c8043ba1942229d341cd0a40dd06a811d4c8e7ec1b39b65f687d5dd08c3764c2180601113ec61b
languageName: node
linkType: hard
"@firebase/app-types@npm:0.9.0":
version: 0.9.0
resolution: "@firebase/app-types@npm:0.9.0"
checksum: e79bd3c4a8d6b911326fe83fddca8d8922ea5880fcb3ad72d3561b51e3d01f22669cdc6d61d2ec48ac9c5e763e3d44b7b6736cadf36a0827d7f62447bde4b12e
languageName: node
linkType: hard
"@firebase/app@npm:0.9.13":
version: 0.9.13
resolution: "@firebase/app@npm:0.9.13"
dependencies:
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
idb: 7.1.1
tslib: ^2.1.0
checksum: 257b65729c4a3bca743c48ef47e23e99087e452e3eef3c3ca54559c394a6bed8570f2bbed2428843a1824dbe69e756bcda33f9b97e530a4a3cba68fea909365c
languageName: node
linkType: hard
"@firebase/auth-compat@npm:0.4.2":
version: 0.4.2
resolution: "@firebase/auth-compat@npm:0.4.2"
dependencies:
"@firebase/auth": 0.23.2
"@firebase/auth-types": 0.12.0
"@firebase/component": 0.6.4
"@firebase/util": 1.9.3
node-fetch: 2.6.7
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: f7911e4c1c593d70195c3fe173d22b734cacc6f8c8a3fb3b016ed8c2916dc236c0a1c2675326ed012c532fdd34aeedfeded5b369749558609147af1552fe0a3c
languageName: node
linkType: hard
"@firebase/auth-interop-types@npm:0.2.1":
version: 0.2.1
resolution: "@firebase/auth-interop-types@npm:0.2.1"
checksum: 6b02996f2455c1d6299c59a76a7d52d3eedd35d6ee444a8f2edef8c34bd766e8d20ea25a6927e08a5f4cfa9a5fff2aa67101a80a7e4d12023590871652eac288
languageName: node
linkType: hard
"@firebase/auth-types@npm:0.12.0":
version: 0.12.0
resolution: "@firebase/auth-types@npm:0.12.0"
peerDependencies:
"@firebase/app-types": 0.x
"@firebase/util": 1.x
checksum: d7eeef6ece62042b7d9a8bd12d5990dc1a2aa6167f2f4dbef43d5713b7f5e06e752e5ea8f1ad56064f58ec085dc0bd6b55e893b0bfd10f13a0a10fbbe70cc303
languageName: node
linkType: hard
"@firebase/auth@npm:0.23.2":
version: 0.23.2
resolution: "@firebase/auth@npm:0.23.2"
dependencies:
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
node-fetch: 2.6.7
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: c5842fc6b8907e938d89886f4bb0008407b132bdcdaede7390f64c052fb48a985ec027eece3d80c23af7e634f4f1d5e7a8e60d9202b31a340721e89604827044
languageName: node
linkType: hard
"@firebase/component@npm:0.6.4":
version: 0.6.4
resolution: "@firebase/component@npm:0.6.4"
dependencies:
"@firebase/util": 1.9.3
tslib: ^2.1.0
checksum: 5d7006e4bc70508f16fe9297c351ca7eff29b59f7fd4cc99a6e28f93b62f422d0401d84b0ddc38a52f7125aa646c9a98d014a86afdd2c50caf178b1987f71ab6
languageName: node
linkType: hard
"@firebase/database-compat@npm:0.3.4":
version: 0.3.4
resolution: "@firebase/database-compat@npm:0.3.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/database": 0.14.4
"@firebase/database-types": 0.10.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
checksum: d5162718f052de9c1c4a6f82c9d42775a2f3dc84f86230a0471eb2c5c50f02837c1bc0be11805867efa2f0798f429443a5a3b9c8670ff34514516abce28ed3f8
languageName: node
linkType: hard
"@firebase/database-types@npm:0.10.4":
version: 0.10.4
resolution: "@firebase/database-types@npm:0.10.4"
dependencies:
"@firebase/app-types": 0.9.0
"@firebase/util": 1.9.3
checksum: 4fcecd212221eced0e84e4b4a3a069ed94cb9060da72472455dd509c4c490417e8929e390937d35e69a5629e4eb490c727bdc1e001ec8f43b097c0734d5715ad
languageName: node
linkType: hard
"@firebase/database@npm:0.14.4":
version: 0.14.4
resolution: "@firebase/database@npm:0.14.4"
dependencies:
"@firebase/auth-interop-types": 0.2.1
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
faye-websocket: 0.11.4
tslib: ^2.1.0
checksum: cc2f520a6b92528589781a7c9d6cbd5409cff89c80d73690903a567ef91bf701d036ef872a1e3bd1797c5a85a64d9dcbf73618973360d3d76282464f06a3ff06
languageName: node
linkType: hard
"@firebase/firestore-compat@npm:0.3.12":
version: 0.3.12
resolution: "@firebase/firestore-compat@npm:0.3.12"
dependencies:
"@firebase/component": 0.6.4
"@firebase/firestore": 3.13.0
"@firebase/firestore-types": 2.5.1
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: 5790e65668ef81e502d68f65b802148c29b990051300d806738a49a2276db1b040c971605fb0a10301e9c2844e7f59cbb96933c3760e5e11ca6f176da6487303
languageName: node
linkType: hard
"@firebase/firestore-types@npm:2.5.1":
version: 2.5.1
resolution: "@firebase/firestore-types@npm:2.5.1"
peerDependencies:
"@firebase/app-types": 0.x
"@firebase/util": 1.x
checksum: 2ad6f26d9dd9fc6f171260b183a2a0372cee4185f5cd56b457239c6e99529cff8391ac9f8278e94c0ab8fcaff80ff78f7f65e7a84091a9c7e7ddf4028c40767b
languageName: node
linkType: hard
"@firebase/firestore@npm:3.13.0":
version: 3.13.0
resolution: "@firebase/firestore@npm:3.13.0"
dependencies:
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
"@firebase/webchannel-wrapper": 0.10.1
"@grpc/grpc-js": ~1.7.0
"@grpc/proto-loader": ^0.6.13
node-fetch: 2.6.7
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: 92beac19b772c88b0d58ffb8d1e4c7052dead32716869a5b54100c538e52b10aed17bf15fc62f36015575047cd282f5380073cc670bc7fcf53a4f637e61ccf8c
languageName: node
linkType: hard
"@firebase/functions-compat@npm:0.3.5":
version: 0.3.5
resolution: "@firebase/functions-compat@npm:0.3.5"
dependencies:
"@firebase/component": 0.6.4
"@firebase/functions": 0.10.0
"@firebase/functions-types": 0.6.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: 7c625b5057593957c3959b8af9a7d42433b83d8fa400abddb587819526b98f5846814a2619a61636934eded066a1d80c215d9418df5f8db5f800e4b70779e13a
languageName: node
linkType: hard
"@firebase/functions-types@npm:0.6.0":
version: 0.6.0
resolution: "@firebase/functions-types@npm:0.6.0"
checksum: 00a2a6db2a92bdaf9334d25ecff005da1a74793e9e16f6a1955720d9f7d2a9db07221231af4494a2b4194024a7f3cfebf918ef992af4fffc9b8a416cec88328e
languageName: node
linkType: hard
"@firebase/functions@npm:0.10.0":
version: 0.10.0
resolution: "@firebase/functions@npm:0.10.0"
dependencies:
"@firebase/app-check-interop-types": 0.3.0
"@firebase/auth-interop-types": 0.2.1
"@firebase/component": 0.6.4
"@firebase/messaging-interop-types": 0.2.0
"@firebase/util": 1.9.3
node-fetch: 2.6.7
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: bdc13250e0b21d100127aefc053cde06fcd183c899fe148f2009e02a2efd223c7a63b1594070a78247381c79979af0ef2535f94a612cf5ec670420d82c86f975
languageName: node
linkType: hard
"@firebase/installations-compat@npm:0.2.4":
version: 0.2.4
resolution: "@firebase/installations-compat@npm:0.2.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/installations": 0.6.4
"@firebase/installations-types": 0.5.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: a5774cf074268d3960709f1603e4fc6d578c73f5b435beeb8b9705e38c51f2c3794cd1846dc696a97a15d9a2e40965a775705770081bbefb71ac1a6a3ef49d2a
languageName: node
linkType: hard
"@firebase/installations-types@npm:0.5.0":
version: 0.5.0
resolution: "@firebase/installations-types@npm:0.5.0"
peerDependencies:
"@firebase/app-types": 0.x
checksum: 6d8449a6d1329b4ca8ce182c61319ff4d5de88864fb2f7f495f2558cc97477e3d21557ffe292194dc37ef498a046c6c5c5c3a54acdecd09ea31a35a6a829dc21
languageName: node
linkType: hard
"@firebase/installations@npm:0.6.4":
version: 0.6.4
resolution: "@firebase/installations@npm:0.6.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/util": 1.9.3
idb: 7.0.1
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: e36cbca01b4a509b44267a6d816352bf32e66b4b749484ea52965a8ddc90ffe08ba773f70353e75f84ba78fcf4d4400beffcdfac2b7efcb6d3240d8235966ea4
languageName: node
linkType: hard
"@firebase/logger@npm:0.4.0":
version: 0.4.0
resolution: "@firebase/logger@npm:0.4.0"
dependencies:
tslib: ^2.1.0
checksum: 4b5418f03a2e973f6d4fa8f3a27057b3cc439691b6067ecfa4755bb310d1ed7bdf53016bc2d13bdbdad7e369485d57e9fd1e4679e30a5b98aab9f87e1fa671ee
languageName: node
linkType: hard
"@firebase/messaging-compat@npm:0.2.4":
version: 0.2.4
resolution: "@firebase/messaging-compat@npm:0.2.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/messaging": 0.12.4
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: 60b0908da24881124df96305a2399df5b3d263285b6c98ae2e59d68819bb42f04ad12b10464046040bee96d32012df70b3017f3f24c975f06b15237ad6f72714
languageName: node
linkType: hard
"@firebase/messaging-interop-types@npm:0.2.0":
version: 0.2.0
resolution: "@firebase/messaging-interop-types@npm:0.2.0"
checksum: 9e489bb4f549415ce0d339816bcd8b042591ede62a37cbb6ebf9355d8dd5bc8abc306bfd9e9041fa192fc0a584b3e8ee5dda704902716b201e14fd2b4a71700d
languageName: node
linkType: hard
"@firebase/messaging@npm:0.12.4":
version: 0.12.4
resolution: "@firebase/messaging@npm:0.12.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/installations": 0.6.4
"@firebase/messaging-interop-types": 0.2.0
"@firebase/util": 1.9.3
idb: 7.0.1
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: 08787e0c0d35ba7231c153f56abb791f9c403550ced3d201818dfcdc1e6befcf393db145d561762729b591f50832bad54caa970d0cebbeee9346551322b8d5fd
languageName: node
linkType: hard
"@firebase/performance-compat@npm:0.2.4":
version: 0.2.4
resolution: "@firebase/performance-compat@npm:0.2.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/performance": 0.6.4
"@firebase/performance-types": 0.2.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: f44a6833f3ec30289d0a934e6748d96b5b233d529c3abfdc7863636f3f4d54683d4b0f6783bee7531d54cd3b8c97f0cc0adf0375021a1021afa823b70820121a
languageName: node
linkType: hard
"@firebase/performance-types@npm:0.2.0":
version: 0.2.0
resolution: "@firebase/performance-types@npm:0.2.0"
checksum: cf7c4ff4eed138642adafc62de28b2dc55fce5d06fb0291a65c79c4ede7b060a0d2282b5534e90269721a3940ef9f3ea4e53308a2a7664a7e6d542924a853edb
languageName: node
linkType: hard
"@firebase/performance@npm:0.6.4":
version: 0.6.4
resolution: "@firebase/performance@npm:0.6.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/installations": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: 3e9829c473e8d05dd09561feee29e51ce86d8ad98517847f30ec1e3c568ad52731053ce69572ea08a5327bfeeefa078a4a01981c9a52a678b78d5fc6c0c7667d
languageName: node
linkType: hard
"@firebase/remote-config-compat@npm:0.2.4":
version: 0.2.4
resolution: "@firebase/remote-config-compat@npm:0.2.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/remote-config": 0.4.4
"@firebase/remote-config-types": 0.3.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: c3e6767fbda1240361925ab1b05e8669189b6df7ff83df120fc880ea8f5d3210e898f8aaee0ba5f8ad70f71a27534e1ae355586475f02d885e23b60e097d965e
languageName: node
linkType: hard
"@firebase/remote-config-types@npm:0.3.0":
version: 0.3.0
resolution: "@firebase/remote-config-types@npm:0.3.0"
checksum: 3ce1b3f17d879e70f235ebbcd14574e2f3be80fcefe88e8d961e17a6453f5fa44694c5892171ec44ef4472df403c3cca3a46828a5b225652ac4d05673a72d01f
languageName: node
linkType: hard
"@firebase/remote-config@npm:0.4.4":
version: 0.4.4
resolution: "@firebase/remote-config@npm:0.4.4"
dependencies:
"@firebase/component": 0.6.4
"@firebase/installations": 0.6.4
"@firebase/logger": 0.4.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: 08b40da1ce426ed5454dcd579f22121a6ebf0b6bd55e28a3fab2542d71ea3ffd864d8acc9348e7b4d7fd10407832ebb424a67374b9e780fc53e6c134fb9fb097
languageName: node
linkType: hard
"@firebase/storage-compat@npm:0.3.2":
version: 0.3.2
resolution: "@firebase/storage-compat@npm:0.3.2"
dependencies:
"@firebase/component": 0.6.4
"@firebase/storage": 0.11.2
"@firebase/storage-types": 0.8.0
"@firebase/util": 1.9.3
tslib: ^2.1.0
peerDependencies:
"@firebase/app-compat": 0.x
checksum: 47d0b71b8c5ff61bb3442505899b2d6d6a804c03463c4a8b40a40a06478043d33b0fe2380f59b4ed259861d16a3c81e1cbb152da1bfbde38ba51e77053cf3917
languageName: node
linkType: hard
"@firebase/storage-types@npm:0.8.0":
version: 0.8.0
resolution: "@firebase/storage-types@npm:0.8.0"
peerDependencies:
"@firebase/app-types": 0.x
"@firebase/util": 1.x
checksum: 05cf05be734c4aac04ee4a7e3008619e18bf4ea79c8feeec803ec8b42367c3669298a9004642df33bf78be4579a230bcf43f53d7196e6577be6e3c854e7a97a5
languageName: node
linkType: hard
"@firebase/storage@npm:0.11.2":
version: 0.11.2
resolution: "@firebase/storage@npm:0.11.2"
dependencies:
"@firebase/component": 0.6.4
"@firebase/util": 1.9.3
node-fetch: 2.6.7
tslib: ^2.1.0
peerDependencies:
"@firebase/app": 0.x
checksum: 0e54b8f7831f89d7cd4b95fb41c1b1fa4a32917f668c59e2c38fcf41c8d11fcd0c6e4c225aa13d0f0244ea58f5d6b40f976d031f14bb07dd02b36375bc415abb
languageName: node
linkType: hard
"@firebase/util@npm:1.9.3":
version: 1.9.3
resolution: "@firebase/util@npm:1.9.3"
dependencies:
tslib: ^2.1.0
checksum: b2dbd39229580df2075d102bc26a895eefdfb7ddc7bd71da6765f9ff4a61f5b67b6583e7e20676c56dc0e3f9379376fdef09a46b37b8d088b9de3eb0afbc066a
languageName: node
linkType: hard
"@firebase/webchannel-wrapper@npm:0.10.1":
version: 0.10.1
resolution: "@firebase/webchannel-wrapper@npm:0.10.1"
checksum: afc9bb7a332dd0de877ba246cd4077e8f0529dc779126d9cf680237b906c6f87ba86c0ebf53f77d8d8e30947725f36b030718f7eb888b86aa0559ae502ee26bf
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.3.0": "@floating-ui/core@npm:^1.3.0":
version: 1.3.0 version: 1.3.0
resolution: "@floating-ui/core@npm:1.3.0" resolution: "@floating-ui/core@npm:1.3.0"
@@ -6118,46 +5610,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@grpc/grpc-js@npm:~1.7.0":
version: 1.7.3
resolution: "@grpc/grpc-js@npm:1.7.3"
dependencies:
"@grpc/proto-loader": ^0.7.0
"@types/node": ">=12.12.47"
checksum: cb05aae4599f5deac9e0f50ea458b4465c581653501b5c1f3f3a9d6bfc5120c731726914d2d0d3a8244fce60cdf86ebbfc69c9d9f39fc34f0ab0100afd4af3e4
languageName: node
linkType: hard
"@grpc/proto-loader@npm:^0.6.13":
version: 0.6.13
resolution: "@grpc/proto-loader@npm:0.6.13"
dependencies:
"@types/long": ^4.0.1
lodash.camelcase: ^4.3.0
long: ^4.0.0
protobufjs: ^6.11.3
yargs: ^16.2.0
bin:
proto-loader-gen-types: build/bin/proto-loader-gen-types.js
checksum: 863417e961cfa3acb579124f5c2bbfbeaee4d507c33470dc0af3b6792892c68706c6c61e26629f5ff3d28cb631dc4f0a00233323135e322406e3cb19a0b92823
languageName: node
linkType: hard
"@grpc/proto-loader@npm:^0.7.0":
version: 0.7.7
resolution: "@grpc/proto-loader@npm:0.7.7"
dependencies:
"@types/long": ^4.0.1
lodash.camelcase: ^4.3.0
long: ^4.0.0
protobufjs: ^7.0.0
yargs: ^17.7.2
bin:
proto-loader-gen-types: build/bin/proto-loader-gen-types.js
checksum: 6015d99d36d0451075a53e5c5842e8912235973a515677afca038269969ad84f22a4c9fbc9badf52f034736b3f1bf864739f7c4238ba8a7e6fd3bba75cfce0ee
languageName: node
linkType: hard
"@hapi/hoek@npm:^9.0.0": "@hapi/hoek@npm:^9.0.0":
version: 9.3.0 version: 9.3.0
resolution: "@hapi/hoek@npm:9.3.0" resolution: "@hapi/hoek@npm:9.3.0"
@@ -12494,7 +11946,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/long@npm:^4.0.0, @types/long@npm:^4.0.1": "@types/long@npm:^4.0.0":
version: 4.0.2 version: 4.0.2
resolution: "@types/long@npm:4.0.2" resolution: "@types/long@npm:4.0.2"
checksum: d16cde7240d834cf44ba1eaec49e78ae3180e724cd667052b194a372f350d024cba8dd3f37b0864931683dab09ca935d52f0c4c1687178af5ada9fc85b0635f4 checksum: d16cde7240d834cf44ba1eaec49e78ae3180e724cd667052b194a372f350d024cba8dd3f37b0864931683dab09ca935d52f0c4c1687178af5ada9fc85b0635f4
@@ -12567,7 +12019,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": "@types/node@npm:*":
version: 20.3.1 version: 20.3.1
resolution: "@types/node@npm:20.3.1" resolution: "@types/node@npm:20.3.1"
checksum: 63a393ab6d947be17320817b35d7277ef03728e231558166ed07ee30b09fd7c08861be4d746f10fdc63ca7912e8cd023939d4eab887ff6580ff704ff24ed810c checksum: 63a393ab6d947be17320817b35d7277ef03728e231558166ed07ee30b09fd7c08861be4d746f10fdc63ca7912e8cd023939d4eab887ff6580ff704ff24ed810c
@@ -19017,15 +18469,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"faye-websocket@npm:0.11.4":
version: 0.11.4
resolution: "faye-websocket@npm:0.11.4"
dependencies:
websocket-driver: ">=0.5.1"
checksum: d49a62caf027f871149fc2b3f3c7104dc6d62744277eb6f9f36e2d5714e847d846b9f7f0d0b7169b25a012e24a594cde11a93034b30732e4c683f20b8a5019fa
languageName: node
linkType: hard
"fb-watchman@npm:^2.0.0": "fb-watchman@npm:^2.0.0":
version: 2.0.2 version: 2.0.2
resolution: "fb-watchman@npm:2.0.2" resolution: "fb-watchman@npm:2.0.2"
@@ -19315,40 +18758,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"firebase@npm:^9.23.0":
version: 9.23.0
resolution: "firebase@npm:9.23.0"
dependencies:
"@firebase/analytics": 0.10.0
"@firebase/analytics-compat": 0.2.6
"@firebase/app": 0.9.13
"@firebase/app-check": 0.8.0
"@firebase/app-check-compat": 0.3.7
"@firebase/app-compat": 0.2.13
"@firebase/app-types": 0.9.0
"@firebase/auth": 0.23.2
"@firebase/auth-compat": 0.4.2
"@firebase/database": 0.14.4
"@firebase/database-compat": 0.3.4
"@firebase/firestore": 3.13.0
"@firebase/firestore-compat": 0.3.12
"@firebase/functions": 0.10.0
"@firebase/functions-compat": 0.3.5
"@firebase/installations": 0.6.4
"@firebase/installations-compat": 0.2.4
"@firebase/messaging": 0.12.4
"@firebase/messaging-compat": 0.2.4
"@firebase/performance": 0.6.4
"@firebase/performance-compat": 0.2.4
"@firebase/remote-config": 0.4.4
"@firebase/remote-config-compat": 0.2.4
"@firebase/storage": 0.11.2
"@firebase/storage-compat": 0.3.2
"@firebase/util": 1.9.3
checksum: 8c3eb314a74d13a08558b6df48e6527a55a90ee7d9c3189d9bc33e10e86d5af8ad5dc8aea7587cc5363b6399e187ba8e580efa4c8469003a84c80f3bea1e7bc6
languageName: node
linkType: hard
"flat-cache@npm:^3.0.4": "flat-cache@npm:^3.0.4":
version: 3.0.4 version: 3.0.4
resolution: "flat-cache@npm:3.0.4" resolution: "flat-cache@npm:3.0.4"
@@ -20798,13 +20207,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"http-parser-js@npm:>=0.5.1":
version: 0.5.8
resolution: "http-parser-js@npm:0.5.8"
checksum: 6bbdf2429858e8cf13c62375b0bfb6dc3955ca0f32e58237488bc86cd2378f31d31785fd3ac4ce93f1c74e0189cf8823c91f5cb061696214fd368d2452dc871d
languageName: node
linkType: hard
"http-proxy-agent@npm:^5.0.0": "http-proxy-agent@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "http-proxy-agent@npm:5.0.0" resolution: "http-proxy-agent@npm:5.0.0"
@@ -20966,14 +20368,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"idb@npm:7.0.1": "idb@npm:^7.1.1":
version: 7.0.1
resolution: "idb@npm:7.0.1"
checksum: 61526789562cc3518a1a030c7a06cc98edfcd62795700ff28c701d6f84c178aee4e98bedfc79e6c394ba26084aa4667d6594b1728e5868f305f9b34148662679
languageName: node
linkType: hard
"idb@npm:7.1.1, idb@npm:^7.1.1":
version: 7.1.1 version: 7.1.1
resolution: "idb@npm:7.1.1" resolution: "idb@npm:7.1.1"
checksum: 1973c28d53c784b177bdef9f527ec89ec239ec7cf5fcbd987dae75a16c03f5b7dfcc8c6d3285716fd0309dd57739805390bd9f98ce23b1b7d8849a3b52de8d56 checksum: 1973c28d53c784b177bdef9f527ec89ec239ec7cf5fcbd987dae75a16c03f5b7dfcc8c6d3285716fd0309dd57739805390bd9f98ce23b1b7d8849a3b52de8d56
@@ -23855,13 +23250,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"long@npm:^5.0.0":
version: 5.2.3
resolution: "long@npm:5.2.3"
checksum: 885ede7c3de4facccbd2cacc6168bae3a02c3e836159ea4252c87b6e34d40af819824b2d4edce330bfb5c4d6e8ce3ec5864bdcf9473fa1f53a4f8225860e5897
languageName: node
linkType: hard
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
version: 1.4.0 version: 1.4.0
resolution: "loose-envify@npm:1.4.0" resolution: "loose-envify@npm:1.4.0"
@@ -26894,50 +26282,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"protobufjs@npm:^6.11.3":
version: 6.11.3
resolution: "protobufjs@npm:6.11.3"
dependencies:
"@protobufjs/aspromise": ^1.1.2
"@protobufjs/base64": ^1.1.2
"@protobufjs/codegen": ^2.0.4
"@protobufjs/eventemitter": ^1.1.0
"@protobufjs/fetch": ^1.1.0
"@protobufjs/float": ^1.0.2
"@protobufjs/inquire": ^1.1.0
"@protobufjs/path": ^1.1.2
"@protobufjs/pool": ^1.1.0
"@protobufjs/utf8": ^1.1.0
"@types/long": ^4.0.1
"@types/node": ">=13.7.0"
long: ^4.0.0
bin:
pbjs: bin/pbjs
pbts: bin/pbts
checksum: 4a6ce1964167e4c45c53fd8a312d7646415c777dd31b4ba346719947b88e61654912326101f927da387d6b6473ab52a7ea4f54d6f15d63b31130ce28e2e15070
languageName: node
linkType: hard
"protobufjs@npm:^7.0.0":
version: 7.2.3
resolution: "protobufjs@npm:7.2.3"
dependencies:
"@protobufjs/aspromise": ^1.1.2
"@protobufjs/base64": ^1.1.2
"@protobufjs/codegen": ^2.0.4
"@protobufjs/eventemitter": ^1.1.0
"@protobufjs/fetch": ^1.1.0
"@protobufjs/float": ^1.0.2
"@protobufjs/inquire": ^1.1.0
"@protobufjs/path": ^1.1.2
"@protobufjs/pool": ^1.1.0
"@protobufjs/utf8": ^1.1.0
"@types/node": ">=13.7.0"
long: ^5.0.0
checksum: 9afa6de5fced0139a5180c063718508fac3ea734a9f1aceb99712367b15473a83327f91193f16b63540f9112b09a40912f5f0441a9b0d3f3c6a1c7f707d78249
languageName: node
linkType: hard
"proxy-addr@npm:~2.0.7": "proxy-addr@npm:~2.0.7":
version: 2.0.7 version: 2.0.7
resolution: "proxy-addr@npm:2.0.7" resolution: "proxy-addr@npm:2.0.7"
@@ -28441,7 +27785,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": "safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0":
version: 5.2.1 version: 5.2.1
resolution: "safe-buffer@npm:5.2.1" resolution: "safe-buffer@npm:5.2.1"
checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491
@@ -31713,24 +31057,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"websocket-driver@npm:>=0.5.1":
version: 0.7.4
resolution: "websocket-driver@npm:0.7.4"
dependencies:
http-parser-js: ">=0.5.1"
safe-buffer: ">=5.1.0"
websocket-extensions: ">=0.1.1"
checksum: fffe5a33fe8eceafd21d2a065661d09e38b93877eae1de6ab5d7d2734c6ed243973beae10ae48c6613cfd675f200e5a058d1e3531bc9e6c5d4f1396ff1f0bfb9
languageName: node
linkType: hard
"websocket-extensions@npm:>=0.1.1":
version: 0.1.4
resolution: "websocket-extensions@npm:0.1.4"
checksum: 5976835e68a86afcd64c7a9762ed85f2f27d48c488c707e67ba85e717b90fa066b98ab33c744d64255c9622d349eedecf728e65a5f921da71b58d0e9591b9038
languageName: node
linkType: hard
"well-known-symbols@npm:^2.0.0": "well-known-symbols@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "well-known-symbols@npm:2.0.0" resolution: "well-known-symbols@npm:2.0.0"