mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 12:36:24 +08:00
refactor(component): make component pure (#5427)
This commit is contained in:
@@ -8,11 +8,9 @@ import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { Schema, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { beforeEach } from 'vitest';
|
||||
|
||||
import { useBlockSuitePagePreview } from '../use-block-suite-page-preview';
|
||||
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
|
||||
|
||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
@@ -49,37 +47,3 @@ describe('useBlockSuiteWorkspacePageTitle', () => {
|
||||
expect(pageTitleHook.result.current).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useBlockSuitePagePreview', () => {
|
||||
test('basic', async () => {
|
||||
const page = blockSuiteWorkspace.getPage('page0') as Page;
|
||||
const id = page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('Hello, world!'),
|
||||
},
|
||||
page.getBlockByFlavour('affine:note')[0].id
|
||||
);
|
||||
const hook = renderHook(() => useAtomValue(useBlockSuitePagePreview(page)));
|
||||
expect(hook.result.current).toBe('Hello, world!');
|
||||
page.transact(() => {
|
||||
page.getBlockById(id)!.text!.insert('Test', 0);
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
hook.rerender();
|
||||
expect(hook.result.current).toBe('TestHello, world!');
|
||||
|
||||
// Insert before
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('First block!'),
|
||||
},
|
||||
page.getBlockByFlavour('affine:note')[0].id,
|
||||
0
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
hook.rerender();
|
||||
expect(hook.result.current).toBe('First block! TestHello, world!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,7 +168,12 @@ export const useAppUpdater = () => {
|
||||
[setSetting]
|
||||
);
|
||||
|
||||
const readChangelog = useAsyncCallback(async () => {
|
||||
const openChangelog = useAsyncCallback(async () => {
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
await setChangelogUnread(true);
|
||||
}, [setChangelogUnread]);
|
||||
|
||||
const dismissChangelog = useAsyncCallback(async () => {
|
||||
await setChangelogUnread(true);
|
||||
}, [setChangelogUnread]);
|
||||
|
||||
@@ -183,7 +188,8 @@ export const useAppUpdater = () => {
|
||||
autoCheck: setting.autoCheckUpdate,
|
||||
autoDownload: setting.autoDownloadUpdate,
|
||||
changelogUnread,
|
||||
readChangelog,
|
||||
openChangelog,
|
||||
dismissChangelog,
|
||||
updateReady,
|
||||
updateAvailable: useAtomValue(updateAvailableAtom),
|
||||
downloadProgress,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
const MAX_PREVIEW_LENGTH = 150;
|
||||
const MAX_SEARCH_BLOCK_COUNT = 30;
|
||||
|
||||
const weakMap = new WeakMap<Page, Atom<string>>();
|
||||
|
||||
export const getPagePreviewText = (page: Page) => {
|
||||
const pageRoot = page.root;
|
||||
if (!pageRoot) {
|
||||
return '';
|
||||
}
|
||||
const preview: string[] = [];
|
||||
// DFS
|
||||
const queue = [pageRoot];
|
||||
let previewLenNeeded = MAX_PREVIEW_LENGTH;
|
||||
let count = MAX_SEARCH_BLOCK_COUNT;
|
||||
while (queue.length && previewLenNeeded > 0 && count-- > 0) {
|
||||
const block = queue.shift();
|
||||
if (!block) {
|
||||
console.error('Unexpected empty block');
|
||||
break;
|
||||
}
|
||||
if (block.children) {
|
||||
queue.unshift(...block.children);
|
||||
}
|
||||
if (block.role !== 'content') {
|
||||
continue;
|
||||
}
|
||||
if (block.text) {
|
||||
const text = block.text.toString();
|
||||
if (!text.length) {
|
||||
continue;
|
||||
}
|
||||
previewLenNeeded -= text.length;
|
||||
preview.push(text);
|
||||
} else {
|
||||
// image/attachment/bookmark
|
||||
const type = block.flavour.split('affine:')[1] ?? null;
|
||||
previewLenNeeded -= type.length + 2;
|
||||
type && preview.push(`[${type}]`);
|
||||
}
|
||||
}
|
||||
return preview.join(' ');
|
||||
};
|
||||
|
||||
const emptyAtom = atom<string>('');
|
||||
|
||||
export function useBlockSuitePagePreview(page: Page | null): Atom<string> {
|
||||
if (page === null) {
|
||||
return emptyAtom;
|
||||
} else if (weakMap.has(page)) {
|
||||
return weakMap.get(page) as Atom<string>;
|
||||
} else {
|
||||
const baseAtom = atom<string>('');
|
||||
baseAtom.onMount = set => {
|
||||
const disposables = [
|
||||
page.slots.ready.on(() => {
|
||||
set(getPagePreviewText(page));
|
||||
}),
|
||||
page.slots.blockUpdated.on(() => {
|
||||
set(getPagePreviewText(page));
|
||||
}),
|
||||
];
|
||||
set(getPagePreviewText(page));
|
||||
return () => {
|
||||
disposables.forEach(disposable => disposable.dispose());
|
||||
};
|
||||
};
|
||||
weakMap.set(page, baseAtom);
|
||||
return baseAtom;
|
||||
}
|
||||
}
|
||||
@@ -1,112 +1,42 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import PQueue from 'p-queue';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const logger = new DebugLogger('use-block-suite-workspace-page');
|
||||
|
||||
const weakMap = new WeakMap<Workspace, Map<string, Atom<Page | null>>>();
|
||||
|
||||
const emptyAtom = atom<Page | null>(null);
|
||||
|
||||
function getAtom(w: Workspace, pageId: string | null): Atom<Page | null> {
|
||||
if (!pageId) {
|
||||
return emptyAtom;
|
||||
}
|
||||
if (!weakMap.has(w)) {
|
||||
weakMap.set(w, new Map());
|
||||
}
|
||||
const map = weakMap.get(w);
|
||||
assertExists(map);
|
||||
if (!map.has(pageId)) {
|
||||
const baseAtom = atom(w.getPage(pageId));
|
||||
baseAtom.onMount = set => {
|
||||
const group = new DisposableGroup();
|
||||
group.add(
|
||||
w.slots.pageAdded.on(id => {
|
||||
if (pageId === id) {
|
||||
set(w.getPage(id));
|
||||
}
|
||||
})
|
||||
);
|
||||
group.add(
|
||||
w.slots.pageRemoved.on(id => {
|
||||
if (pageId === id) {
|
||||
set(null);
|
||||
}
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
group.dispose();
|
||||
};
|
||||
};
|
||||
map.set(pageId, baseAtom);
|
||||
return baseAtom;
|
||||
} else {
|
||||
return map.get(pageId) as Atom<Page | null>;
|
||||
}
|
||||
}
|
||||
// concurrently load 3 pages at most
|
||||
const CONCURRENT_JOBS = 3;
|
||||
|
||||
const loadPageQueue = new PQueue({
|
||||
concurrency: CONCURRENT_JOBS,
|
||||
});
|
||||
|
||||
const loadedPages = new WeakSet<Page>();
|
||||
|
||||
const awaitForIdle = () =>
|
||||
new Promise(resolve =>
|
||||
requestIdleCallback(resolve, {
|
||||
timeout: 1000, // do not wait for too long
|
||||
})
|
||||
);
|
||||
|
||||
const awaitForTimeout = (timeout: number) =>
|
||||
new Promise(resolve => setTimeout(resolve, timeout));
|
||||
|
||||
/**
|
||||
* Load a page and wait for it to be loaded
|
||||
* This page will be loaded in a queue so that it will not jam the network and browser CPU
|
||||
*/
|
||||
export function loadPage(page: Page, priority = 0) {
|
||||
if (loadedPages.has(page)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
loadedPages.add(page);
|
||||
return loadPageQueue.add(
|
||||
async () => {
|
||||
if (!page.loaded) {
|
||||
await awaitForIdle();
|
||||
await page.waitForLoaded();
|
||||
logger.debug('page loaded', page.id);
|
||||
// we do not know how long it takes to load a page here
|
||||
// so that we just use 300ms timeout as the default page processing time
|
||||
await awaitForTimeout(300);
|
||||
} else {
|
||||
// do nothing if it is already loaded
|
||||
}
|
||||
},
|
||||
{
|
||||
priority,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useBlockSuiteWorkspacePage(
|
||||
blockSuiteWorkspace: Workspace,
|
||||
pageId: string | null
|
||||
): Page | null {
|
||||
const pageAtom = getAtom(blockSuiteWorkspace, pageId);
|
||||
assertExists(pageAtom);
|
||||
const page = useAtomValue(pageAtom);
|
||||
const [page, setPage] = useState(
|
||||
pageId ? blockSuiteWorkspace.getPage(pageId) : null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const group = new DisposableGroup();
|
||||
group.add(
|
||||
blockSuiteWorkspace.slots.pageAdded.on(id => {
|
||||
if (pageId === id) {
|
||||
setPage(blockSuiteWorkspace.getPage(id));
|
||||
}
|
||||
})
|
||||
);
|
||||
group.add(
|
||||
blockSuiteWorkspace.slots.pageRemoved.on(id => {
|
||||
if (pageId === id) {
|
||||
setPage(null);
|
||||
}
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
group.dispose();
|
||||
};
|
||||
}, [blockSuiteWorkspace, pageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page && !page.loaded) {
|
||||
loadPage(page).catch(err => {
|
||||
page.load().catch(err => {
|
||||
logger.error('Failed to load page', err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
|
||||
import type { 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
|
||||
) {
|
||||
import { useWorkspaceBlobObjectUrl } from './use-workspace-blob';
|
||||
|
||||
export function useWorkspaceInfo(meta: WorkspaceMetadata) {
|
||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||
|
||||
const [information, setInformation] = useState(
|
||||
@@ -20,7 +19,20 @@ export function useWorkspaceInfo(
|
||||
return information.onUpdated.on(info => {
|
||||
setInformation(info);
|
||||
}).dispose;
|
||||
}, [meta, workspace, workspaceManager]);
|
||||
}, [meta, workspaceManager]);
|
||||
|
||||
return information;
|
||||
}
|
||||
|
||||
export function useWorkspaceName(meta: WorkspaceMetadata) {
|
||||
const information = useWorkspaceInfo(meta);
|
||||
|
||||
return information?.name;
|
||||
}
|
||||
|
||||
export function useWorkspaceAvatar(meta: WorkspaceMetadata) {
|
||||
const information = useWorkspaceInfo(meta);
|
||||
const avatar = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user