mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 18:02:47 +08:00
feat: add affine global channel (#1762)
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
import type { WorkspaceFlavour } from '@affine/workspace/type';
|
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
import type { EditorContainer } from '@blocksuite/editor';
|
import type { EditorContainer } from '@blocksuite/editor';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { atom, createStore } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
import { unstable_batchedUpdates } from 'react-dom';
|
import { unstable_batchedUpdates } from 'react-dom';
|
||||||
|
|
||||||
import { WorkspacePlugins } from '../plugins';
|
import { WorkspacePlugins } from '../plugins';
|
||||||
import type { RemWorkspace } from '../shared';
|
import type { AllWorkspace } from '../shared';
|
||||||
// workspace necessary atoms
|
// workspace necessary atoms
|
||||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
||||||
export const currentPageIdAtom = atom<string | null>(null);
|
export const currentPageIdAtom = atom<string | null>(null);
|
||||||
@@ -34,19 +34,7 @@ export const openWorkspacesModalAtom = atom(false);
|
|||||||
export const openCreateWorkspaceModalAtom = atom(false);
|
export const openCreateWorkspaceModalAtom = atom(false);
|
||||||
export const openQuickSearchModalAtom = atom(false);
|
export const openQuickSearchModalAtom = atom(false);
|
||||||
|
|
||||||
export const jotaiStore = createStore();
|
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
|
||||||
|
|
||||||
type JotaiWorkspace = {
|
|
||||||
id: string;
|
|
||||||
flavour: WorkspaceFlavour;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const jotaiWorkspacesAtom = atomWithStorage<JotaiWorkspace[]>(
|
|
||||||
'jotai-workspaces',
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
export const workspacesAtom = atom<Promise<RemWorkspace[]>>(async get => {
|
|
||||||
const flavours: string[] = Object.values(WorkspacePlugins).map(
|
const flavours: string[] = Object.values(WorkspacePlugins).map(
|
||||||
plugin => plugin.flavour
|
plugin => plugin.flavour
|
||||||
);
|
);
|
||||||
@@ -62,7 +50,7 @@ export const workspacesAtom = atom<Promise<RemWorkspace[]>>(async get => {
|
|||||||
return CRUD.get(workspace.id);
|
return CRUD.get(workspace.id);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return workspaces.filter(workspace => workspace !== null) as RemWorkspace[];
|
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
|
||||||
});
|
});
|
||||||
|
|
||||||
type View = { id: string; mode: 'page' | 'edgeless' };
|
type View = { id: string; mode: 'page' | 'edgeless' };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { config } from '@affine/env';
|
import { config } from '@affine/env';
|
||||||
|
import type { Provider } from '@affine/workspace/type';
|
||||||
|
|
||||||
import type { BlockSuiteWorkspace, Provider } from '../shared';
|
import type { BlockSuiteWorkspace } from '../shared';
|
||||||
import {
|
import {
|
||||||
createAffineWebSocketProvider,
|
createAffineWebSocketProvider,
|
||||||
createBroadCastChannelProvider,
|
createBroadCastChannelProvider,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import type { AffineDownloadProvider } from '@affine/workspace/type';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { AffineDownloadProvider } from '../../../shared';
|
|
||||||
import { BlockSuiteWorkspace } from '../../../shared';
|
import { BlockSuiteWorkspace } from '../../../shared';
|
||||||
import { affineApis } from '../../../shared/apis';
|
import { affineApis } from '../../../shared/apis';
|
||||||
import { providerLogger } from '../../logger';
|
import { providerLogger } from '../../logger';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { BroadCastChannelProvider } from '@affine/workspace/type';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import type { Awareness } from 'y-protocols/awareness';
|
import type { Awareness } from 'y-protocols/awareness';
|
||||||
import {
|
import {
|
||||||
@@ -5,7 +6,6 @@ import {
|
|||||||
encodeAwarenessUpdate,
|
encodeAwarenessUpdate,
|
||||||
} from 'y-protocols/awareness';
|
} from 'y-protocols/awareness';
|
||||||
|
|
||||||
import type { BroadCastChannelProvider } from '../../../shared';
|
|
||||||
import { BlockSuiteWorkspace } from '../../../shared';
|
import { BlockSuiteWorkspace } from '../../../shared';
|
||||||
import { providerLogger } from '../../logger';
|
import { providerLogger } from '../../logger';
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { KeckProvider } from '@affine/workspace/affine/keck';
|
import { KeckProvider } from '@affine/workspace/affine/keck';
|
||||||
import { getLoginStorage } from '@affine/workspace/affine/login';
|
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||||
|
import type {
|
||||||
|
AffineWebSocketProvider,
|
||||||
|
LocalIndexedDBProvider,
|
||||||
|
} from '@affine/workspace/type';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||||
|
|
||||||
import type {
|
import type { BlockSuiteWorkspace } from '../../shared';
|
||||||
AffineWebSocketProvider,
|
|
||||||
BlockSuiteWorkspace,
|
|
||||||
LocalIndexedDBProvider,
|
|
||||||
} from '../../shared';
|
|
||||||
import { providerLogger } from '../logger';
|
import { providerLogger } from '../logger';
|
||||||
import { createBroadCastChannelProvider } from './broad-cast-channel';
|
import { createBroadCastChannelProvider } from './broad-cast-channel';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
import { PermissionType } from '@affine/workspace/affine/api';
|
import { PermissionType } from '@affine/workspace/affine/api';
|
||||||
|
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import {
|
import {
|
||||||
DeleteTemporarilyIcon,
|
DeleteTemporarilyIcon,
|
||||||
@@ -11,7 +12,6 @@ import type React from 'react';
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { useMembers } from '../../../../../hooks/affine/use-members';
|
import { useMembers } from '../../../../../hooks/affine/use-members';
|
||||||
import type { AffineWorkspace, LocalWorkspace } from '../../../../../shared';
|
|
||||||
import { toast } from '../../../../../utils';
|
import { toast } from '../../../../../utils';
|
||||||
import { Unreachable } from '../../../affine-error-eoundary';
|
import { Unreachable } from '../../../affine-error-eoundary';
|
||||||
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
|
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
|
||||||
|
|||||||
@@ -6,17 +6,14 @@ import {
|
|||||||
Wrapper,
|
Wrapper,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish';
|
import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish';
|
||||||
import type {
|
import type { AffineOfficialWorkspace } from '../../../../../shared';
|
||||||
AffineOfficialWorkspace,
|
|
||||||
AffineWorkspace,
|
|
||||||
LocalWorkspace,
|
|
||||||
} from '../../../../../shared';
|
|
||||||
import { toast } from '../../../../../utils';
|
import { toast } from '../../../../../utils';
|
||||||
import { Unreachable } from '../../../affine-error-eoundary';
|
import { Unreachable } from '../../../affine-error-eoundary';
|
||||||
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
setLoginStorage,
|
setLoginStorage,
|
||||||
SignMethod,
|
SignMethod,
|
||||||
} from '@affine/workspace/affine/login';
|
} from '@affine/workspace/affine/login';
|
||||||
|
import type { LocalWorkspace } from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import {
|
import {
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
@@ -18,10 +19,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { affineAuth } from '../../../../hooks/affine/use-affine-log-in';
|
import { affineAuth } from '../../../../hooks/affine/use-affine-log-in';
|
||||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||||
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
|
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
|
||||||
import type {
|
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||||
AffineOfficialWorkspace,
|
|
||||||
LocalWorkspace,
|
|
||||||
} from '../../../../shared';
|
|
||||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||||
|
|
||||||
const IconWrapper = styled('div')(({ theme }) => {
|
const IconWrapper = styled('div')(({ theme }) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type React from 'react';
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { useBlockSuiteWorkspaceAvatarUrl } from '../../../hooks/use-blocksuite-workspace-avatar-url';
|
import { useBlockSuiteWorkspaceAvatarUrl } from '../../../hooks/use-blocksuite-workspace-avatar-url';
|
||||||
import type { BlockSuiteWorkspace, RemWorkspace } from '../../../shared';
|
import type { AllWorkspace, BlockSuiteWorkspace } from '../../../shared';
|
||||||
import { stringToColour } from '../../../utils';
|
import { stringToColour } from '../../../utils';
|
||||||
|
|
||||||
interface AvatarProps {
|
interface AvatarProps {
|
||||||
@@ -75,7 +75,7 @@ export const Avatar: React.FC<AvatarProps> = memo<AvatarProps>(function Avatar({
|
|||||||
|
|
||||||
export type WorkspaceUnitAvatarProps = {
|
export type WorkspaceUnitAvatarProps = {
|
||||||
size?: number;
|
size?: number;
|
||||||
workspace: RemWorkspace | null;
|
workspace: AllWorkspace | null;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type React from 'react';
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useBlockSuiteWorkspaceName } from '../../../hooks/use-blocksuite-workspace-name';
|
import { useBlockSuiteWorkspaceName } from '../../../hooks/use-blocksuite-workspace-name';
|
||||||
import type { RemWorkspace } from '../../../shared';
|
import type { AllWorkspace } from '../../../shared';
|
||||||
import {
|
import {
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
JoinedWorkspaceIcon,
|
JoinedWorkspaceIcon,
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
StyleWorkspaceTitle,
|
StyleWorkspaceTitle,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
export type WorkspaceTypeProps = {
|
export type WorkspaceTypeProps = {
|
||||||
workspace: RemWorkspace;
|
workspace: AllWorkspace;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WorkspaceType: React.FC<WorkspaceTypeProps> = ({ workspace }) => {
|
const WorkspaceType: React.FC<WorkspaceTypeProps> = ({ workspace }) => {
|
||||||
@@ -58,9 +58,9 @@ const WorkspaceType: React.FC<WorkspaceTypeProps> = ({ workspace }) => {
|
|||||||
|
|
||||||
export type WorkspaceCardProps = {
|
export type WorkspaceCardProps = {
|
||||||
currentWorkspaceId: string | null;
|
currentWorkspaceId: string | null;
|
||||||
workspace: RemWorkspace;
|
workspace: AllWorkspace;
|
||||||
onClick: (workspace: RemWorkspace) => void;
|
onClick: (workspace: AllWorkspace) => void;
|
||||||
onSettingClick: (workspace: RemWorkspace) => void;
|
onSettingClick: (workspace: AllWorkspace) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceCard: React.FC<WorkspaceCardProps> = ({
|
export const WorkspaceCard: React.FC<WorkspaceCardProps> = ({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useTranslation } from '@affine/i18n';
|
|||||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||||
import { HelpIcon, PlusIcon } from '@blocksuite/icons';
|
import { HelpIcon, PlusIcon } from '@blocksuite/icons';
|
||||||
|
|
||||||
import type { RemWorkspace } from '../../../shared';
|
import type { AllWorkspace } from '../../../shared';
|
||||||
import { Footer } from '../footer';
|
import { Footer } from '../footer';
|
||||||
import { WorkspaceCard } from '../workspace-card';
|
import { WorkspaceCard } from '../workspace-card';
|
||||||
import { LanguageMenu } from './language-menu';
|
import { LanguageMenu } from './language-menu';
|
||||||
@@ -28,12 +28,12 @@ import {
|
|||||||
|
|
||||||
interface WorkspaceModalProps {
|
interface WorkspaceModalProps {
|
||||||
user: AccessTokenMessage | null;
|
user: AccessTokenMessage | null;
|
||||||
workspaces: RemWorkspace[];
|
workspaces: AllWorkspace[];
|
||||||
currentWorkspaceId: RemWorkspace['id'] | null;
|
currentWorkspaceId: AllWorkspace['id'] | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onClickWorkspace: (workspace: RemWorkspace) => void;
|
onClickWorkspace: (workspace: AllWorkspace) => void;
|
||||||
onClickWorkspaceSetting: (workspace: RemWorkspace) => void;
|
onClickWorkspaceSetting: (workspace: AllWorkspace) => void;
|
||||||
onClickLogin: () => void;
|
onClickLogin: () => void;
|
||||||
onClickLogout: () => void;
|
onClickLogout: () => void;
|
||||||
onCreateWorkspace: () => void;
|
onCreateWorkspace: () => void;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { PageMeta } from '@blocksuite/store';
|
|||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type { RemWorkspace } from '../../../shared';
|
import type { AllWorkspace } from '../../../shared';
|
||||||
import type { TreeNode } from '../../affine/pivots';
|
import type { TreeNode } from '../../affine/pivots';
|
||||||
import {
|
import {
|
||||||
PivotRender,
|
PivotRender,
|
||||||
@@ -20,7 +20,7 @@ export const PivotInternal = ({
|
|||||||
openPage,
|
openPage,
|
||||||
allMetas,
|
allMetas,
|
||||||
}: {
|
}: {
|
||||||
currentWorkspace: RemWorkspace;
|
currentWorkspace: AllWorkspace;
|
||||||
openPage: (pageId: string) => void;
|
openPage: (pageId: string) => void;
|
||||||
allMetas: PageMeta[];
|
allMetas: PageMeta[];
|
||||||
}) => {
|
}) => {
|
||||||
@@ -68,7 +68,7 @@ export const Pivots = ({
|
|||||||
openPage,
|
openPage,
|
||||||
allMetas,
|
allMetas,
|
||||||
}: {
|
}: {
|
||||||
currentWorkspace: RemWorkspace;
|
currentWorkspace: AllWorkspace;
|
||||||
openPage: (pageId: string) => void;
|
openPage: (pageId: string) => void;
|
||||||
allMetas: PageMeta[];
|
allMetas: PageMeta[];
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type React from 'react';
|
|||||||
|
|
||||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||||
import { useBlockSuiteWorkspaceName } from '../../../../hooks/use-blocksuite-workspace-name';
|
import { useBlockSuiteWorkspaceName } from '../../../../hooks/use-blocksuite-workspace-name';
|
||||||
import type { RemWorkspace } from '../../../../shared';
|
import type { AllWorkspace } from '../../../../shared';
|
||||||
import { WorkspaceAvatar } from '../../workspace-avatar';
|
import { WorkspaceAvatar } from '../../workspace-avatar';
|
||||||
import {
|
import {
|
||||||
StyledSelectorContainer,
|
StyledSelectorContainer,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from './styles';
|
} from './styles';
|
||||||
|
|
||||||
export type WorkspaceSelectorProps = {
|
export type WorkspaceSelectorProps = {
|
||||||
currentWorkspace: RemWorkspace | null;
|
currentWorkspace: AllWorkspace | null;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
|
import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
|
||||||
import { usePageMeta } from '../../../hooks/use-page-meta';
|
import { usePageMeta } from '../../../hooks/use-page-meta';
|
||||||
import type { RemWorkspace } from '../../../shared';
|
import type { AllWorkspace } from '../../../shared';
|
||||||
import { SidebarSwitch } from '../../affine/sidebar-switch';
|
import { SidebarSwitch } from '../../affine/sidebar-switch';
|
||||||
import { ChangeLog } from './changeLog';
|
import { ChangeLog } from './changeLog';
|
||||||
import Favorite from './favorite';
|
import Favorite from './favorite';
|
||||||
@@ -39,7 +39,7 @@ export type WorkSpaceSliderBarProps = {
|
|||||||
isPublicWorkspace: boolean;
|
isPublicWorkspace: boolean;
|
||||||
onOpenQuickSearchModal: () => void;
|
onOpenQuickSearchModal: () => void;
|
||||||
onOpenWorkspaceListModal: () => void;
|
onOpenWorkspaceListModal: () => void;
|
||||||
currentWorkspace: RemWorkspace | null;
|
currentWorkspace: AllWorkspace | null;
|
||||||
currentPageId: string | null;
|
currentPageId: string | null;
|
||||||
openPage: (pageId: string) => void;
|
openPage: (pageId: string) => void;
|
||||||
createPage: () => Page;
|
createPage: () => Page;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'fake-indexeddb/auto';
|
|||||||
|
|
||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
|
|
||||||
|
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
|
import type { LocalWorkspace } from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
@@ -17,13 +19,8 @@ import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
import {
|
import { currentWorkspaceIdAtom, workspacesAtom } from '../../atoms';
|
||||||
currentWorkspaceIdAtom,
|
|
||||||
jotaiWorkspacesAtom,
|
|
||||||
workspacesAtom,
|
|
||||||
} from '../../atoms';
|
|
||||||
import { LocalPlugin } from '../../plugins/local';
|
import { LocalPlugin } from '../../plugins/local';
|
||||||
import type { LocalWorkspace } from '../../shared';
|
|
||||||
import { BlockSuiteWorkspace, WorkspaceSubPath } from '../../shared';
|
import { BlockSuiteWorkspace, WorkspaceSubPath } from '../../shared';
|
||||||
import {
|
import {
|
||||||
useGuideHidden,
|
useGuideHidden,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||||
import { clearLoginStorage } from '@affine/workspace/affine/login';
|
import { clearLoginStorage } from '@affine/workspace/affine/login';
|
||||||
|
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { jotaiWorkspacesAtom } from '../../atoms';
|
|
||||||
import { WorkspacePlugins } from '../../plugins';
|
import { WorkspacePlugins } from '../../plugins';
|
||||||
|
|
||||||
export function useAffineLogOut() {
|
export function useAffineLogOut() {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
|
import type { AffineWorkspace } from '@affine/workspace/type';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms';
|
|
||||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||||
import type { AffineWorkspace } from '../../shared';
|
|
||||||
import { affineApis } from '../../shared/apis';
|
import { affineApis } from '../../shared/apis';
|
||||||
|
|
||||||
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
currentWorkspaceIdAtom,
|
currentWorkspaceIdAtom,
|
||||||
workspacesAtom,
|
workspacesAtom,
|
||||||
} from '../../atoms';
|
} from '../../atoms';
|
||||||
import type { RemWorkspace } from '../../shared';
|
import type { AllWorkspace } from '../../shared';
|
||||||
|
|
||||||
export const currentWorkspaceAtom = atom<Promise<RemWorkspace | null>>(
|
export const currentWorkspaceAtom = atom<Promise<AllWorkspace | null>>(
|
||||||
async get => {
|
async get => {
|
||||||
const id = get(currentWorkspaceIdAtom);
|
const id = get(currentWorkspaceIdAtom);
|
||||||
const workspaces = await get(workspacesAtom);
|
const workspaces = await get(workspacesAtom);
|
||||||
@@ -23,7 +23,7 @@ export const lastWorkspaceIdAtom = atomWithStorage<string | null>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export function useCurrentWorkspace(): [
|
export function useCurrentWorkspace(): [
|
||||||
RemWorkspace | null,
|
AllWorkspace | null,
|
||||||
(id: string | null) => void
|
(id: string | null) => void
|
||||||
] {
|
] {
|
||||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
||||||
|
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { jotaiWorkspacesAtom } from '../atoms';
|
|
||||||
import { LocalPlugin } from '../plugins/local';
|
import { LocalPlugin } from '../plugins/local';
|
||||||
|
|
||||||
export function useCreateFirstWorkspace() {
|
export function useCreateFirstWorkspace() {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { jotaiStore } from '@affine/workspace/atom';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import type { NextRouter } from 'next/router';
|
import type { NextRouter } from 'next/router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { currentPageIdAtom, jotaiStore } from '../atoms';
|
import { currentPageIdAtom } from '../atoms';
|
||||||
import type { RemWorkspace } from '../shared';
|
import type { AllWorkspace } from '../shared';
|
||||||
import { WorkspaceSubPath } from '../shared';
|
import { WorkspaceSubPath } from '../shared';
|
||||||
import { useCurrentPageId } from './current/use-current-page-id';
|
import { useCurrentPageId } from './current/use-current-page-id';
|
||||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||||
@@ -11,7 +12,7 @@ import { RouteLogic, useRouterHelper } from './use-router-helper';
|
|||||||
import { useWorkspaces } from './use-workspaces';
|
import { useWorkspaces } from './use-workspaces';
|
||||||
|
|
||||||
export function findSuitablePageId(
|
export function findSuitablePageId(
|
||||||
workspace: RemWorkspace,
|
workspace: AllWorkspace,
|
||||||
targetId: string
|
targetId: string
|
||||||
): string | null {
|
): string | null {
|
||||||
switch (workspace.flavour) {
|
switch (workspace.flavour) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
import type { WorkspaceFlavour } from '@affine/workspace/type';
|
import type { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import type { WorkspaceRegistry } from '@affine/workspace/type';
|
import type { WorkspaceRegistry } from '@affine/workspace/type';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { jotaiWorkspacesAtom } from '../atoms';
|
|
||||||
import { WorkspacePlugins } from '../plugins';
|
import { WorkspacePlugins } from '../plugins';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
|
import type { LocalWorkspace } from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { nanoid } from '@blocksuite/store';
|
import { nanoid } from '@blocksuite/store';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms';
|
import { workspacesAtom } from '../atoms';
|
||||||
import { WorkspacePlugins } from '../plugins';
|
import { WorkspacePlugins } from '../plugins';
|
||||||
import { LocalPlugin } from '../plugins/local';
|
import { LocalPlugin } from '../plugins/local';
|
||||||
import type { LocalWorkspace, RemWorkspace } from '../shared';
|
import type { AllWorkspace } from '../shared';
|
||||||
|
|
||||||
export function useWorkspaces(): RemWorkspace[] {
|
export function useWorkspaces(): AllWorkspace[] {
|
||||||
return useAtomValue(workspacesAtom);
|
return useAtomValue(workspacesAtom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { setUpLanguage, useTranslation } from '@affine/i18n';
|
import { setUpLanguage, useTranslation } from '@affine/i18n';
|
||||||
|
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
|
||||||
|
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { assertExists, nanoid } from '@blocksuite/store';
|
import { assertExists, nanoid } from '@blocksuite/store';
|
||||||
import { NoSsr } from '@mui/material';
|
import { NoSsr } from '@mui/material';
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
@@ -11,7 +14,6 @@ import { Suspense, useCallback, useEffect } from 'react';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
currentWorkspaceIdAtom,
|
currentWorkspaceIdAtom,
|
||||||
jotaiWorkspacesAtom,
|
|
||||||
openQuickSearchModalAtom,
|
openQuickSearchModalAtom,
|
||||||
openWorkspacesModalAtom,
|
openWorkspacesModalAtom,
|
||||||
workspaceLockAtom,
|
workspaceLockAtom,
|
||||||
@@ -33,13 +35,13 @@ import { useRouterTitle } from '../hooks/use-router-title';
|
|||||||
import { useWorkspaces } from '../hooks/use-workspaces';
|
import { useWorkspaces } from '../hooks/use-workspaces';
|
||||||
import { WorkspacePlugins } from '../plugins';
|
import { WorkspacePlugins } from '../plugins';
|
||||||
import { ModalProvider } from '../providers/ModalProvider';
|
import { ModalProvider } from '../providers/ModalProvider';
|
||||||
import type { RemWorkspace } from '../shared';
|
import type { AllWorkspace } from '../shared';
|
||||||
import { pathGenerator, publicPathGenerator } from '../shared';
|
import { pathGenerator, publicPathGenerator } from '../shared';
|
||||||
import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles';
|
import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var currentWorkspace: RemWorkspace;
|
var currentWorkspace: AllWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuickSearchModal = dynamic(
|
const QuickSearchModal = dynamic(
|
||||||
@@ -89,6 +91,10 @@ export const QuickSearch: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const logger = new DebugLogger('workspace-layout');
|
const logger = new DebugLogger('workspace-layout');
|
||||||
|
|
||||||
|
const affineGlobalChannel = createAffineGlobalChannel(
|
||||||
|
WorkspacePlugins[WorkspaceFlavour.AFFINE].CRUD
|
||||||
|
);
|
||||||
export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
|
export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
|
||||||
function WorkspacesSuspense({ children }) {
|
function WorkspacesSuspense({ children }) {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
@@ -105,6 +111,7 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
|
|||||||
const lists = Object.values(WorkspacePlugins)
|
const lists = Object.values(WorkspacePlugins)
|
||||||
.sort((a, b) => a.loadPriority - b.loadPriority)
|
.sort((a, b) => a.loadPriority - b.loadPriority)
|
||||||
.map(({ CRUD }) => CRUD.list);
|
.map(({ CRUD }) => CRUD.list);
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
const items = [];
|
const items = [];
|
||||||
for (const list of lists) {
|
for (const list of lists) {
|
||||||
@@ -121,6 +128,7 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
|
|||||||
set([...items]);
|
set([...items]);
|
||||||
logger.info('mount first data:', items);
|
logger.info('mount first data:', items);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch();
|
fetch();
|
||||||
return () => {
|
return () => {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
@@ -128,6 +136,19 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
|
|||||||
};
|
};
|
||||||
}, [set]);
|
}, [set]);
|
||||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||||
|
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const flavour = jotaiWorkspaces.find(
|
||||||
|
x => x.id === currentWorkspaceId
|
||||||
|
)?.flavour;
|
||||||
|
if (flavour === WorkspaceFlavour.AFFINE) {
|
||||||
|
affineGlobalChannel.connect();
|
||||||
|
return () => {
|
||||||
|
affineGlobalChannel.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [currentWorkspaceId, jotaiWorkspaces]);
|
||||||
return (
|
return (
|
||||||
<NoSsr>
|
<NoSsr>
|
||||||
{/* fixme(himself65): don't re-render whole modals */}
|
{/* fixme(himself65): don't re-render whole modals */}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import '../styles/globals.css';
|
|||||||
|
|
||||||
import { config, setupGlobal } from '@affine/env';
|
import { config, setupGlobal } from '@affine/env';
|
||||||
import { createI18n, I18nextProvider } from '@affine/i18n';
|
import { createI18n, I18nextProvider } from '@affine/i18n';
|
||||||
|
import { jotaiStore } from '@affine/workspace/atom';
|
||||||
import type { EmotionCache } from '@emotion/cache';
|
import type { EmotionCache } from '@emotion/cache';
|
||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import { Provider } from 'jotai';
|
import { Provider } from 'jotai';
|
||||||
@@ -11,7 +12,6 @@ import { useRouter } from 'next/router';
|
|||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import React, { Suspense, useEffect, useMemo } from 'react';
|
import React, { Suspense, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { jotaiStore } from '../atoms';
|
|
||||||
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
|
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
|
||||||
import { ProviderComposer } from '../components/provider-composer';
|
import { ProviderComposer } from '../components/provider-composer';
|
||||||
import { PageLoading } from '../components/pure/loading';
|
import { PageLoading } from '../components/pure/loading';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button } from '@affine/component';
|
import { Button } from '@affine/component';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import type { BroadCastChannelProvider } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { nanoid } from '@blocksuite/store';
|
import { nanoid } from '@blocksuite/store';
|
||||||
import { Typography } from '@mui/material';
|
import { Typography } from '@mui/material';
|
||||||
@@ -9,7 +10,6 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { createBroadCastChannelProvider } from '../../blocksuite/providers';
|
import { createBroadCastChannelProvider } from '../../blocksuite/providers';
|
||||||
import PageList from '../../components/blocksuite/block-suite-page-list/page-list';
|
import PageList from '../../components/blocksuite/block-suite-page-list/page-list';
|
||||||
import { StyledPage, StyledWrapper } from '../../layouts/styles';
|
import { StyledPage, StyledWrapper } from '../../layouts/styles';
|
||||||
import type { BroadCastChannelProvider } from '../../shared';
|
|
||||||
import { toast } from '../../utils';
|
import { toast } from '../../utils';
|
||||||
|
|
||||||
const logger = new DebugLogger('broadcast');
|
const logger = new DebugLogger('broadcast');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { FolderIcon } from '@blocksuite/icons';
|
import { FolderIcon } from '@blocksuite/icons';
|
||||||
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
||||||
@@ -17,10 +18,7 @@ import { useRouterHelper } from '../../../hooks/use-router-helper';
|
|||||||
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||||
import { WorkspaceLayout } from '../../../layouts';
|
import { WorkspaceLayout } from '../../../layouts';
|
||||||
import { WorkspacePlugins } from '../../../plugins';
|
import { WorkspacePlugins } from '../../../plugins';
|
||||||
import type {
|
import type { NextPageWithLayout } from '../../../shared';
|
||||||
LocalIndexedDBProvider,
|
|
||||||
NextPageWithLayout,
|
|
||||||
} from '../../../shared';
|
|
||||||
|
|
||||||
const AllPage: NextPageWithLayout = () => {
|
const AllPage: NextPageWithLayout = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { getLoginStorage } from '@affine/workspace/affine/login';
|
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||||
|
import { jotaiStore } from '@affine/workspace/atom';
|
||||||
|
import type { AffineWorkspace } from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
|
|
||||||
import { jotaiStore, workspacesAtom } from '../../atoms';
|
import { workspacesAtom } from '../../atoms';
|
||||||
import { createAffineProviders } from '../../blocksuite';
|
import { createAffineProviders } from '../../blocksuite';
|
||||||
import { Unreachable } from '../../components/affine/affine-error-eoundary';
|
import { Unreachable } from '../../components/affine/affine-error-eoundary';
|
||||||
import type { AffineWorkspace } from '../../shared';
|
|
||||||
import { affineApis } from '../../shared/apis';
|
import { affineApis } from '../../shared/apis';
|
||||||
|
|
||||||
type Query = (typeof QueryKey)[keyof typeof QueryKey];
|
type Query = (typeof QueryKey)[keyof typeof QueryKey];
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getLoginStorage } from '@affine/workspace/affine/login';
|
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||||
|
import type { AffineWorkspace } from '@affine/workspace/type';
|
||||||
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { createJSONStorage } from 'jotai/utils';
|
import { createJSONStorage } from 'jotai/utils';
|
||||||
@@ -11,7 +12,6 @@ import { PageNotFoundError } from '../../components/affine/affine-error-eoundary
|
|||||||
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
|
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
|
||||||
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
||||||
import { PageDetailEditor } from '../../components/page-detail-editor';
|
import { PageDetailEditor } from '../../components/page-detail-editor';
|
||||||
import type { AffineWorkspace } from '../../shared';
|
|
||||||
import { BlockSuiteWorkspace } from '../../shared';
|
import { BlockSuiteWorkspace } from '../../shared';
|
||||||
import { affineApis } from '../../shared/apis';
|
import { affineApis } from '../../shared/apis';
|
||||||
import { initPage } from '../../utils';
|
import { initPage } from '../../utils';
|
||||||
|
|||||||
@@ -5,17 +5,9 @@ import type {
|
|||||||
} from '@affine/workspace/type';
|
} from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
|
|
||||||
import type { AffineWorkspace, LocalWorkspace } from '../shared';
|
|
||||||
import { AffinePlugin } from './affine';
|
import { AffinePlugin } from './affine';
|
||||||
import { LocalPlugin } from './local';
|
import { LocalPlugin } from './local';
|
||||||
|
|
||||||
declare module '@affine/workspace/type' {
|
|
||||||
interface WorkspaceRegistry {
|
|
||||||
[WorkspaceFlavour.AFFINE]: AffineWorkspace;
|
|
||||||
[WorkspaceFlavour.LOCAL]: LocalWorkspace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspacePlugin<Flavour extends WorkspaceFlavour> {
|
export interface WorkspacePlugin<Flavour extends WorkspaceFlavour> {
|
||||||
flavour: Flavour;
|
flavour: Flavour;
|
||||||
// Plugin will be loaded according to the priority
|
// Plugin will be loaded according to the priority
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
||||||
|
import type { LocalWorkspace } from '@affine/workspace/type';
|
||||||
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { nanoid } from '@blocksuite/store';
|
import { nanoid } from '@blocksuite/store';
|
||||||
@@ -12,9 +13,8 @@ import { PageNotFoundError } from '../../components/affine/affine-error-eoundary
|
|||||||
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
|
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
|
||||||
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
||||||
import { PageDetailEditor } from '../../components/page-detail-editor';
|
import { PageDetailEditor } from '../../components/page-detail-editor';
|
||||||
import type { LocalWorkspace } from '../../shared';
|
|
||||||
import { BlockSuiteWorkspace } from '../../shared';
|
import { BlockSuiteWorkspace } from '../../shared';
|
||||||
import { initPage } from '../../utils/blocksuite';
|
import { initPage } from '../../utils';
|
||||||
import type { WorkspacePlugin } from '..';
|
import type { WorkspacePlugin } from '..';
|
||||||
|
|
||||||
const getStorage = () => createJSONStorage(() => localStorage);
|
const getStorage = () => createJSONStorage(() => localStorage);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||||
import type { LoginResponse } from '@affine/workspace/affine/login';
|
import type { LoginResponse } from '@affine/workspace/affine/login';
|
||||||
import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login';
|
import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login';
|
||||||
|
import { jotaiStore } from '@affine/workspace/atom';
|
||||||
|
|
||||||
import { jotaiStore } from '../atoms';
|
|
||||||
import { isValidIPAddress } from '../utils';
|
import { isValidIPAddress } from '../utils';
|
||||||
|
|
||||||
let prefixUrl = '/';
|
let prefixUrl = '/';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api';
|
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||||
import type { WorkspaceFlavour } from '@affine/workspace/type';
|
|
||||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
import type { NextPage } from 'next';
|
import type { NextPage } from 'next';
|
||||||
import type { ReactElement, ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
@@ -12,59 +11,9 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AffineWorkspace extends RemoteWorkspace {
|
|
||||||
flavour: WorkspaceFlavour.AFFINE;
|
|
||||||
// empty
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
providers: Provider[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LocalWorkspace {
|
|
||||||
flavour: WorkspaceFlavour.LOCAL;
|
|
||||||
id: string;
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
providers: Provider[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BaseProvider = {
|
|
||||||
flavour: string;
|
|
||||||
// if this is true, we will connect the provider on the background
|
|
||||||
background: boolean;
|
|
||||||
connect: () => void;
|
|
||||||
disconnect: () => void;
|
|
||||||
// cleanup data when workspace is removed
|
|
||||||
cleanup: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface BackgroundProvider extends BaseProvider {
|
|
||||||
background: true;
|
|
||||||
callbacks: Set<() => void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AffineDownloadProvider extends BaseProvider {
|
|
||||||
flavour: 'affine-download';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BroadCastChannelProvider extends BaseProvider {
|
|
||||||
flavour: 'broadcast-channel';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LocalIndexedDBProvider extends BackgroundProvider {
|
|
||||||
flavour: 'local-indexeddb';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AffineWebSocketProvider extends BaseProvider {
|
|
||||||
flavour: 'affine-websocket';
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Provider =
|
|
||||||
| LocalIndexedDBProvider
|
|
||||||
| AffineWebSocketProvider
|
|
||||||
| BroadCastChannelProvider;
|
|
||||||
|
|
||||||
export type AffineOfficialWorkspace = AffineWorkspace | LocalWorkspace;
|
export type AffineOfficialWorkspace = AffineWorkspace | LocalWorkspace;
|
||||||
|
|
||||||
export type RemWorkspace = AffineOfficialWorkspace;
|
export type AllWorkspace = AffineOfficialWorkspace;
|
||||||
|
|
||||||
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
|
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
|
||||||
P,
|
P,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "@affine/workspace",
|
"name": "@affine/workspace",
|
||||||
"private": true,
|
"private": true,
|
||||||
"exports": {
|
"exports": {
|
||||||
|
"./atom": "./src/atom.ts",
|
||||||
"./utils": "./src/utils.ts",
|
"./utils": "./src/utils.ts",
|
||||||
"./type": "./src/type.ts",
|
"./type": "./src/type.ts",
|
||||||
"./affine/*": "./src/affine/*.ts",
|
"./affine/*": "./src/affine/*.ts",
|
||||||
|
|||||||
@@ -94,17 +94,30 @@ export enum PermissionType {
|
|||||||
Owner = 99,
|
Owner = 99,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Workspace {
|
export const userSchema = z.object({
|
||||||
id: string;
|
id: z.string(),
|
||||||
type: WorkspaceType;
|
name: z.string(),
|
||||||
public: boolean;
|
email: z.string(),
|
||||||
permission: PermissionType;
|
avatar_url: z.string(),
|
||||||
}
|
create_at: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export interface WorkspaceDetail extends Workspace {
|
export const workspaceSchema = z.object({
|
||||||
owner: User;
|
id: z.string(),
|
||||||
member_count: number;
|
type: z.nativeEnum(WorkspaceType),
|
||||||
}
|
public: z.boolean(),
|
||||||
|
permission: z.nativeEnum(PermissionType),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Workspace = z.infer<typeof workspaceSchema>;
|
||||||
|
|
||||||
|
export const workspaceDetailSchema = z.object({
|
||||||
|
...workspaceSchema.shape,
|
||||||
|
owner: userSchema,
|
||||||
|
member_count: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WorkspaceDetail = z.infer<typeof workspaceDetailSchema>;
|
||||||
|
|
||||||
export interface Permission {
|
export interface Permission {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
93
packages/workspace/src/affine/channel.ts
Normal file
93
packages/workspace/src/affine/channel.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import {
|
||||||
|
getLoginStorage,
|
||||||
|
isExpired,
|
||||||
|
parseIdToken,
|
||||||
|
} from '@affine/workspace/affine/login';
|
||||||
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import * as url from 'lib0/url';
|
||||||
|
import * as websocket from 'lib0/websocket';
|
||||||
|
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
71
packages/workspace/src/affine/sync.ts
Normal file
71
packages/workspace/src/affine/sync.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
workspaceDetailSchema,
|
||||||
|
workspaceSchema,
|
||||||
|
} from '@affine/workspace/affine/api';
|
||||||
|
import { WebsocketClient } from '@affine/workspace/affine/channel';
|
||||||
|
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||||
|
import type { WorkspaceCRUD } from '@affine/workspace/type';
|
||||||
|
import type { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const channelMessageSchema = z.object({
|
||||||
|
ws_list: z.array(workspaceSchema),
|
||||||
|
ws_details: z.record(workspaceDetailSchema),
|
||||||
|
metadata: z.record(
|
||||||
|
z.object({
|
||||||
|
avatar: 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) {
|
||||||
|
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) {
|
||||||
|
jotaiStore.set(jotaiWorkspacesAtom, workspaces => {
|
||||||
|
const idx = workspaces.findIndex(workspace => workspace.id === id);
|
||||||
|
workspaces.splice(idx, 1);
|
||||||
|
return [...workspaces];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect: () => {
|
||||||
|
client = new WebsocketClient(
|
||||||
|
`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
||||||
|
window.location.host
|
||||||
|
}/api/global/sync`
|
||||||
|
);
|
||||||
|
client.connect(handleMessage);
|
||||||
|
},
|
||||||
|
disconnect: () => {
|
||||||
|
assertExists(client, 'client is null');
|
||||||
|
client.disconnect();
|
||||||
|
client = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
18
packages/workspace/src/atom.ts
Normal file
18
packages/workspace/src/atom.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
|
import { createStore } from 'jotai/index';
|
||||||
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
|
export type JotaiWorkspace = {
|
||||||
|
id: string;
|
||||||
|
flavour: WorkspaceFlavour;
|
||||||
|
};
|
||||||
|
|
||||||
|
// root primitive atom that stores the list of workspaces which could be used in the app
|
||||||
|
// if a workspace is not in this list, it should not be used in the app
|
||||||
|
export const jotaiWorkspacesAtom = atomWithStorage<JotaiWorkspace[]>(
|
||||||
|
'jotai-workspaces',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// global jotai store, which is used to store all the atoms
|
||||||
|
export const jotaiStore = createStore();
|
||||||
@@ -1,6 +1,57 @@
|
|||||||
|
import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api';
|
||||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
export type BaseProvider = {
|
||||||
|
flavour: string;
|
||||||
|
// if this is true, we will connect the provider on the background
|
||||||
|
background: boolean;
|
||||||
|
connect: () => void;
|
||||||
|
disconnect: () => void;
|
||||||
|
// cleanup data when workspace is removed
|
||||||
|
cleanup: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BackgroundProvider extends BaseProvider {
|
||||||
|
background: true;
|
||||||
|
callbacks: Set<() => void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffineDownloadProvider extends BaseProvider {
|
||||||
|
flavour: 'affine-download';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BroadCastChannelProvider extends BaseProvider {
|
||||||
|
flavour: 'broadcast-channel';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalIndexedDBProvider extends BackgroundProvider {
|
||||||
|
flavour: 'local-indexeddb';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffineWebSocketProvider extends BaseProvider {
|
||||||
|
flavour: 'affine-websocket';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Provider =
|
||||||
|
| LocalIndexedDBProvider
|
||||||
|
| AffineWebSocketProvider
|
||||||
|
| BroadCastChannelProvider;
|
||||||
|
|
||||||
|
export interface AffineWorkspace extends RemoteWorkspace {
|
||||||
|
flavour: WorkspaceFlavour.AFFINE;
|
||||||
|
// empty
|
||||||
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
|
providers: Provider[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalWorkspace {
|
||||||
|
flavour: WorkspaceFlavour.LOCAL;
|
||||||
|
id: string;
|
||||||
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
|
providers: Provider[];
|
||||||
|
}
|
||||||
|
|
||||||
export const enum LoadPriority {
|
export const enum LoadPriority {
|
||||||
HIGH = 1,
|
HIGH = 1,
|
||||||
MEDIUM = 2,
|
MEDIUM = 2,
|
||||||
@@ -11,6 +62,7 @@ export const enum WorkspaceFlavour {
|
|||||||
AFFINE = 'affine',
|
AFFINE = 'affine',
|
||||||
LOCAL = 'local',
|
LOCAL = 'local',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingPanel = {
|
export const settingPanel = {
|
||||||
General: 'general',
|
General: 'general',
|
||||||
Collaboration: 'collaboration',
|
Collaboration: 'collaboration',
|
||||||
@@ -21,8 +73,11 @@ export const settingPanel = {
|
|||||||
export const settingPanelValues = [...Object.values(settingPanel)] as const;
|
export const settingPanelValues = [...Object.values(settingPanel)] as const;
|
||||||
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
|
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// built-in workspaces
|
||||||
export interface WorkspaceRegistry {}
|
export interface WorkspaceRegistry {
|
||||||
|
[WorkspaceFlavour.AFFINE]: AffineWorkspace;
|
||||||
|
[WorkspaceFlavour.LOCAL]: LocalWorkspace;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> {
|
export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> {
|
||||||
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
|
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
|
||||||
|
|||||||
Reference in New Issue
Block a user