mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor: workspace manager (#5060)
This commit is contained in:
@@ -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(() =>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
40
packages/frontend/hooks/src/use-workspace-blob.ts
Normal file
40
packages/frontend/hooks/src/use-workspace-blob.ts
Normal 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;
|
||||
}
|
||||
26
packages/frontend/hooks/src/use-workspace-info.ts
Normal file
26
packages/frontend/hooks/src/use-workspace-info.ts
Normal 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;
|
||||
}
|
||||
34
packages/frontend/hooks/src/use-workspace-status.ts
Normal file
34
packages/frontend/hooks/src/use-workspace-status.ts
Normal 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;
|
||||
}
|
||||
28
packages/frontend/hooks/src/use-workspace.ts
Normal file
28
packages/frontend/hooks/src/use-workspace.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user