feat: add affine global channel (#1762)

This commit is contained in:
Himself65
2023-03-30 18:21:26 -05:00
committed by GitHub
parent 3fa7d17dca
commit bb1224f9ee
38 changed files with 358 additions and 162 deletions

View File

@@ -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' };

View File

@@ -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,

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 }) => {

View File

@@ -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;
}; };

View File

@@ -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> = ({

View File

@@ -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;

View File

@@ -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[];
}) => { }) => {

View File

@@ -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;
}; };

View File

@@ -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;

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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';
/** /**

View File

@@ -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);
} }

View File

@@ -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 */}

View File

@@ -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';

View File

@@ -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');

View File

@@ -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();

View File

@@ -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];

View File

@@ -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';

View File

@@ -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

View File

@@ -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);

View File

@@ -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 = '/';

View File

@@ -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,

View File

@@ -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",

View File

@@ -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;

View 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);
}
}
}
});
}
}

View 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;
},
};
}

View 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();

View File

@@ -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>;