refactor: workspace manager (#5060)

This commit is contained in:
EYHN
2023-12-15 07:20:50 +00:00
parent af15aa06d4
commit fe2851d3e9
217 changed files with 3605 additions and 4244 deletions

View File

@@ -13,7 +13,6 @@ import { describe, expect, test, vi } from 'vitest';
import { beforeEach } from 'vitest';
import { useBlockSuitePagePreview } from '../use-block-suite-page-preview';
import { useBlockSuiteWorkspaceName } from '../use-block-suite-workspace-name';
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
let blockSuiteWorkspace: BlockSuiteWorkspace;
@@ -39,21 +38,6 @@ beforeEach(async () => {
await initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
});
describe('useBlockSuiteWorkspaceName', () => {
test('basic', async () => {
blockSuiteWorkspace.meta.setName('test 1');
const workspaceNameHook = renderHook(() =>
useBlockSuiteWorkspaceName(blockSuiteWorkspace)
);
expect(workspaceNameHook.result.current[0]).toBe('test 1');
blockSuiteWorkspace.meta.setName('test 2');
workspaceNameHook.rerender();
expect(workspaceNameHook.result.current[0]).toBe('test 2');
workspaceNameHook.result.current[1]('test 3');
expect(blockSuiteWorkspace.meta.name).toBe('test 3');
});
});
describe('useBlockSuiteWorkspacePageTitle', () => {
test('basic', async () => {
const pageTitleHook = renderHook(() =>

View File

@@ -14,6 +14,7 @@ export function useBlockSuitePageMeta(
const baseAtom = atom<PageMeta[]>(blockSuiteWorkspace.meta.pageMetas);
weakMap.set(blockSuiteWorkspace, baseAtom);
baseAtom.onMount = set => {
set(blockSuiteWorkspace.meta.pageMetas);
const dispose = blockSuiteWorkspace.meta.pageMetasUpdated.on(() => {
set(blockSuiteWorkspace.meta.pageMetas);
});

View File

@@ -1,102 +0,0 @@
import { assertExists } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import reduce from 'image-blob-reduce';
import { useCallback, useEffect, useState } from 'react';
import useSWRImmutable from 'swr/immutable';
// validate and reduce image size and return as file
export const validateAndReduceImage = async (file: File): Promise<File> => {
// Declare a new async function that wraps the decode logic
const decodeAndReduceImage = async (): Promise<Blob> => {
const img = new Image();
const url = URL.createObjectURL(file);
img.src = url;
await img.decode().catch(() => {
URL.revokeObjectURL(url);
throw new Error('Image could not be decoded');
});
img.onload = img.onerror = () => {
URL.revokeObjectURL(url);
};
const sizeInMB = file.size / (1024 * 1024);
if (sizeInMB > 10 || img.width > 4000 || img.height > 4000) {
// Compress the file to less than 10MB
const compressedImg = await reduce().toBlob(file, {
max: 4000,
unsharpAmount: 80,
unsharpRadius: 0.6,
unsharpThreshold: 2,
});
return compressedImg;
}
return file;
};
try {
const reducedBlob = await decodeAndReduceImage();
return new File([reducedBlob], file.name, { type: file.type });
} catch (error) {
throw new Error('Image could not be reduce :' + error);
}
};
export function useBlockSuiteWorkspaceAvatarUrl(
blockSuiteWorkspace: Workspace
) {
const [url, set] = useState(() => blockSuiteWorkspace.meta.avatar);
if (url !== blockSuiteWorkspace.meta.avatar) {
set(blockSuiteWorkspace.meta.avatar);
}
const { data: avatar, mutate } = useSWRImmutable(url, {
fetcher: async avatar => {
assertExists(blockSuiteWorkspace);
const blobs = blockSuiteWorkspace.blob;
const blob = await blobs.get(avatar);
if (blob) {
return URL.createObjectURL(blob);
}
return null;
},
suspense: false,
});
const setAvatar = useCallback(
async (file: File | null): Promise<boolean> => {
assertExists(blockSuiteWorkspace);
if (!file) {
blockSuiteWorkspace.meta.setAvatar('');
return false;
}
try {
const reducedFile = await validateAndReduceImage(file);
const blobs = blockSuiteWorkspace.blob;
const blobId = await blobs.set(reducedFile);
blockSuiteWorkspace.meta.setAvatar(blobId);
await mutate(blobId);
return true;
} catch (error) {
console.error(error);
throw error;
}
},
[blockSuiteWorkspace, mutate]
);
useEffect(() => {
if (blockSuiteWorkspace) {
const dispose = blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => {
set(blockSuiteWorkspace.meta.avatar);
});
return () => {
dispose.dispose();
};
}
return;
}, [blockSuiteWorkspace]);
return [avatar ?? null, setAvatar] as const;
}

View File

@@ -1,37 +0,0 @@
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import type { Workspace } from '@blocksuite/store';
import type { Atom, WritableAtom } from 'jotai';
import { atom, useAtom } from 'jotai';
type StringAtom = WritableAtom<string, [string], void> & Atom<string>;
const weakMap = new WeakMap<Workspace, StringAtom>();
export function useBlockSuiteWorkspaceName(blockSuiteWorkspace: Workspace) {
let nameAtom: StringAtom;
if (!weakMap.has(blockSuiteWorkspace)) {
const baseAtom = atom<string>(
blockSuiteWorkspace.meta.name ?? UNTITLED_WORKSPACE_NAME
);
const writableAtom = atom(
get => get(baseAtom),
(_, set, name: string) => {
blockSuiteWorkspace.meta.setName(name);
set(baseAtom, name);
}
);
baseAtom.onMount = set => {
const dispose = blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => {
set(blockSuiteWorkspace.meta.name ?? '');
});
return () => {
dispose.dispose();
};
};
weakMap.set(blockSuiteWorkspace, writableAtom);
nameAtom = writableAtom;
} else {
nameAtom = weakMap.get(blockSuiteWorkspace) as StringAtom;
}
return useAtom(nameAtom);
}

View File

@@ -0,0 +1,40 @@
import { workspaceManagerAtom } from '@affine/workspace/atom';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
export function useWorkspaceBlobObjectUrl(
meta?: WorkspaceMetadata,
blobKey?: string | null
) {
const workspaceManager = useAtomValue(workspaceManagerAtom);
const [blob, setBlob] = useState<string | undefined>(undefined);
useEffect(() => {
setBlob(undefined);
if (!blobKey || !meta) {
return;
}
let canceled = false;
let objectUrl: string = '';
workspaceManager
.getWorkspaceBlob(meta, blobKey)
.then(blob => {
if (blob && !canceled) {
objectUrl = URL.createObjectURL(blob);
setBlob(objectUrl);
}
})
.catch(err => {
console.error('get workspace blob error: ' + err);
});
return () => {
canceled = true;
URL.revokeObjectURL(objectUrl);
};
}, [meta, blobKey, workspaceManager]);
return blob;
}

View File

@@ -0,0 +1,26 @@
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
import { workspaceManagerAtom } from '@affine/workspace/atom';
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
export function useWorkspaceInfo(
meta: WorkspaceMetadata,
workspace?: Workspace
) {
const workspaceManager = useAtomValue(workspaceManagerAtom);
const [information, setInformation] = useState(
() => workspaceManager.list.getInformation(meta).info
);
useEffect(() => {
const information = workspaceManager.list.getInformation(meta);
setInformation(information.info);
return information.onUpdated.on(info => {
setInformation(info);
}).dispose;
}, [meta, workspace, workspaceManager]);
return information;
}

View File

@@ -0,0 +1,34 @@
import type { Workspace, WorkspaceStatus } from '@affine/workspace';
import { useEffect, useState } from 'react';
export function useWorkspaceStatus<
Selector extends ((status: WorkspaceStatus) => any) | undefined | null,
Status = Selector extends (status: WorkspaceStatus) => any
? ReturnType<Selector>
: WorkspaceStatus,
>(workspace?: Workspace | null, selector?: Selector): Status | null {
// avoid re-render when selector is changed
const [cachedSelector] = useState(() => selector);
const [status, setStatus] = useState<Status | null>(() => {
if (!workspace) {
return null;
}
return cachedSelector ? cachedSelector(workspace.status) : workspace.status;
});
useEffect(() => {
if (!workspace) {
setStatus(null);
return;
}
setStatus(
cachedSelector ? cachedSelector(workspace.status) : workspace.status
);
return workspace.onStatusChange.on(status =>
setStatus(cachedSelector ? cachedSelector(status) : status)
).dispose;
}, [cachedSelector, workspace]);
return status;
}

View File

@@ -0,0 +1,28 @@
import type { Workspace } from '@affine/workspace';
import { workspaceManagerAtom } from '@affine/workspace/atom';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
/**
* definitely be careful when using this hook, open workspace is a heavy operation
*/
export function useWorkspace(meta?: WorkspaceMetadata | null) {
const workspaceManager = useAtomValue(workspaceManagerAtom);
const [workspace, setWorkspace] = useState<Workspace | null>(null);
useEffect(() => {
if (!meta) {
setWorkspace(null); // set to null if meta is null or undefined
return;
}
const ref = workspaceManager.use(meta);
setWorkspace(ref.workspace);
return () => {
ref.release();
};
}, [meta, workspaceManager]);
return workspace;
}