refactor(core): move hooks to core (#5458)

* move @toeverything/hooks -> @affine/core/hooks
* delete @toeverything/hooks

hooks are all business-related logic and are deeply coupled with other parts.

Move them into the core and then reconstruct them by feature.
This commit is contained in:
EYHN
2024-01-02 08:05:46 +00:00
parent 6862b7deaf
commit 4b217e6b89
95 changed files with 99 additions and 347 deletions

View File

@@ -0,0 +1,47 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { Schema, Workspace } from '@blocksuite/store';
import { renderHook } from '@testing-library/react';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { beforeEach, describe, expect, test } from 'vitest';
import { useBlockSuitePageMeta } from '../use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '../use-block-suite-workspace-helper';
let blockSuiteWorkspace: Workspace;
const schema = new Schema();
schema.register(AffineSchemas).register(__unstableSchemas);
beforeEach(async () => {
blockSuiteWorkspace = new Workspace({
id: 'test',
schema,
});
await initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
await initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
await initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
});
describe('useBlockSuiteWorkspaceHelper', () => {
test('should create page', () => {
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3);
const helperHook = renderHook(() =>
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace)
);
const pageMetaHook = renderHook(() =>
useBlockSuitePageMeta(blockSuiteWorkspace)
);
expect(pageMetaHook.result.current.length).toBe(3);
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3);
const page = helperHook.result.current.createPage('page4');
expect(page.id).toBe('page4');
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(4);
pageMetaHook.rerender();
expect(pageMetaHook.result.current.length).toBe(4);
});
});

View File

@@ -0,0 +1,49 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
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 { describe, expect, test, vi } from 'vitest';
import { beforeEach } from 'vitest';
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
let blockSuiteWorkspace: BlockSuiteWorkspace;
const schema = new Schema();
schema.register(AffineSchemas).register(__unstableSchemas);
beforeEach(async () => {
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test', schema });
const initPage = async (page: Page) => {
await page.waitForLoaded();
expect(page).not.toBeNull();
assertExists(page);
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(''),
});
const frameId = page.addBlock('affine:note', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
};
await initPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
await initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
await initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
});
describe('useBlockSuiteWorkspacePageTitle', () => {
test('basic', async () => {
const pageTitleHook = renderHook(() =>
useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, 'page0')
);
expect(pageTitleHook.result.current).toBe('Untitled');
blockSuiteWorkspace.setPageMeta('page0', { title: '1' });
pageTitleHook.rerender();
expect(pageTitleHook.result.current).toBe('1');
});
});

View File

@@ -0,0 +1,29 @@
import React from 'react';
export type AsyncErrorHandler = (error: Error) => void;
/**
* App should provide a global error handler for async callback in the root.
*/
export const AsyncCallbackContext = React.createContext<AsyncErrorHandler>(
e => {
console.error(e);
}
);
/**
* Translate async function to sync function and handle error automatically.
* Only accept void function, return data here is meaningless.
*/
export function useAsyncCallback<T extends any[]>(
callback: (...args: T) => Promise<void>,
deps: any[]
): (...args: T) => void {
const handleAsyncError = React.useContext(AsyncCallbackContext);
return React.useCallback(
(...args: any) => {
callback(...args).catch(e => handleAsyncError(e));
},
[...deps] // eslint-disable-line react-hooks/exhaustive-deps
);
}

View File

@@ -3,10 +3,10 @@ import {
type AllPageListConfig,
FavoriteTag,
} from '@affine/component/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useCallback, useMemo } from 'react';

View File

@@ -1,9 +1,9 @@
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
} from '@affine/core/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';

View File

@@ -1,5 +1,5 @@
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { LOCALES, useI18N } from '@affine/i18n';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useMemo } from 'react';
export function useLanguageHelper() {

View File

@@ -1,10 +1,10 @@
import { toast } from '@affine/component';
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import {
PreconditionStrategy,
registerAffineCommand,

View File

@@ -1,9 +1,9 @@
import { toast } from '@affine/component';
import type { DraggableTitleCellData } from '@affine/component/page-list';
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';

View File

@@ -1,5 +1,5 @@
import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useAtomValue } from 'jotai';
import { currentPageIdAtom } from '../../atoms/mode';

View File

@@ -0,0 +1,198 @@
import { apis, events, type UpdateMeta } from '@affine/electron-api';
import { isBrowser } from '@affine/env/constant';
import { appSettingAtom } from '@toeverything/infra/atom';
import { atom, useAtom, useAtomValue } from 'jotai';
import { atomWithObservable, atomWithStorage } from 'jotai/utils';
import { useCallback, useState } from 'react';
import { Observable } from 'rxjs';
import { useAsyncCallback } from './affine-async-hooks';
function rpcToObservable<
T,
H extends () => Promise<T>,
E extends (callback: (t: T) => void) => () => void,
>(
initialValue: T | null,
{
event,
handler,
onSubscribe,
}: {
event?: E;
handler?: H;
onSubscribe?: () => void;
}
): Observable<T | null> {
return new Observable<T | null>(subscriber => {
subscriber.next(initialValue);
onSubscribe?.();
if (!isBrowser || !environment.isDesktop || !event) {
subscriber.complete();
return;
}
handler?.()
.then(t => {
subscriber.next(t);
})
.catch(err => {
subscriber.error(err);
});
return event(t => {
subscriber.next(t);
});
});
}
// download complete, ready to install
export const updateReadyAtom = atomWithObservable(() => {
return rpcToObservable(null as UpdateMeta | null, {
event: events?.updater.onUpdateReady,
});
});
// update available, but not downloaded yet
export const updateAvailableAtom = atomWithObservable(() => {
return rpcToObservable(null as UpdateMeta | null, {
event: events?.updater.onUpdateAvailable,
});
});
// downloading new update
export const downloadProgressAtom = atomWithObservable(() => {
return rpcToObservable(null as number | null, {
event: events?.updater.onDownloadProgress,
});
});
export const changelogCheckedAtom = atomWithStorage<Record<string, boolean>>(
'affine:client-changelog-checked',
{}
);
export const checkingForUpdatesAtom = atom(false);
export const currentVersionAtom = atom(async () => {
if (!isBrowser) {
return null;
}
const currentVersion = await apis?.updater.currentVersion();
return currentVersion;
});
const currentChangelogUnreadAtom = atom(
async get => {
if (!isBrowser) {
return false;
}
const mapping = get(changelogCheckedAtom);
const currentVersion = await get(currentVersionAtom);
if (currentVersion) {
return !mapping[currentVersion];
}
return false;
},
async (get, set, v: boolean) => {
const currentVersion = await get(currentVersionAtom);
if (currentVersion) {
set(changelogCheckedAtom, mapping => {
return {
...mapping,
[currentVersion]: v,
};
});
}
}
);
export const useAppUpdater = () => {
const [appQuitting, setAppQuitting] = useState(false);
const updateReady = useAtomValue(updateReadyAtom);
const [setting, setSetting] = useAtom(appSettingAtom);
const downloadProgress = useAtomValue(downloadProgressAtom);
const [changelogUnread, setChangelogUnread] = useAtom(
currentChangelogUnreadAtom
);
const [checkingForUpdates, setCheckingForUpdates] = useAtom(
checkingForUpdatesAtom
);
const quitAndInstall = useCallback(() => {
if (updateReady) {
setAppQuitting(true);
apis?.updater.quitAndInstall().catch(err => {
// TODO: add error toast here
console.error(err);
});
}
}, [updateReady]);
const checkForUpdates = useCallback(async () => {
if (checkingForUpdates) {
return;
}
setCheckingForUpdates(true);
try {
const updateInfo = await apis?.updater.checkForUpdates();
return updateInfo?.version ?? false;
} catch (err) {
console.error('Error checking for updates:', err);
return null;
} finally {
setCheckingForUpdates(false);
}
}, [checkingForUpdates, setCheckingForUpdates]);
const downloadUpdate = useCallback(() => {
apis?.updater.downloadUpdate().catch(err => {
console.error('Error downloading update:', err);
});
}, []);
const toggleAutoDownload = useCallback(
(enable: boolean) => {
setSetting({
autoDownloadUpdate: enable,
});
},
[setSetting]
);
const toggleAutoCheck = useCallback(
(enable: boolean) => {
setSetting({
autoCheckUpdate: enable,
});
},
[setSetting]
);
const openChangelog = useAsyncCallback(async () => {
window.open(runtimeConfig.changelogUrl, '_blank');
await setChangelogUnread(true);
}, [setChangelogUnread]);
const dismissChangelog = useAsyncCallback(async () => {
await setChangelogUnread(true);
}, [setChangelogUnread]);
return {
quitAndInstall,
checkForUpdates,
downloadUpdate,
toggleAutoDownload,
toggleAutoCheck,
appQuitting,
checkingForUpdates,
autoCheck: setting.autoCheckUpdate,
autoDownload: setting.autoDownloadUpdate,
changelogUnread,
openChangelog,
dismissChangelog,
updateReady,
updateAvailable: useAtomValue(updateAvailableAtom),
downloadProgress,
currentVersion: useAtomValue(currentVersionAtom),
};
};

View File

@@ -0,0 +1,15 @@
import type { AffineEditorContainer } from '@blocksuite/presets';
import { atom, type SetStateAction, useAtom } from 'jotai';
const activeEditorContainerAtom = atom<AffineEditorContainer | null>(null);
export function useActiveBlocksuiteEditor(): [
AffineEditorContainer | null,
React.Dispatch<SetStateAction<AffineEditorContainer | null>>,
] {
const [editorContainer, setEditorContainer] = useAtom(
activeEditorContainerAtom
);
return [editorContainer, setEditorContainer];
}

View File

@@ -0,0 +1,59 @@
import type { PageBlockModel } from '@blocksuite/blocks';
import { assertExists } from '@blocksuite/global/utils';
import type { PageMeta, Workspace } from '@blocksuite/store';
import type { Atom } from 'jotai';
import { atom, useAtomValue } from 'jotai';
import { useMemo } from 'react';
const weakMap = new WeakMap<Workspace, Atom<PageMeta[]>>();
export function useBlockSuitePageMeta(
blockSuiteWorkspace: Workspace
): PageMeta[] {
if (!weakMap.has(blockSuiteWorkspace)) {
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);
});
return () => {
dispose.dispose();
};
};
}
return useAtomValue(weakMap.get(blockSuiteWorkspace) as Atom<PageMeta[]>);
}
export function usePageMetaHelper(blockSuiteWorkspace: Workspace) {
return useMemo(
() => ({
setPageTitle: (pageId: string, newTitle: string) => {
const page = blockSuiteWorkspace.getPage(pageId);
assertExists(page);
const pageBlock = page
.getBlockByFlavour('affine:page')
.at(0) as PageBlockModel;
assertExists(pageBlock);
page.transact(() => {
pageBlock.title.delete(0, pageBlock.title.length);
pageBlock.title.insert(newTitle, 0);
});
blockSuiteWorkspace.meta.setPageMeta(pageId, { title: newTitle });
},
setPageReadonly: (pageId: string, readonly: boolean) => {
const page = blockSuiteWorkspace.getPage(pageId);
assertExists(page);
page.awarenessStore.setReadonly(page, readonly);
},
setPageMeta: (pageId: string, pageMeta: Partial<PageMeta>) => {
blockSuiteWorkspace.meta.setPageMeta(pageId, pageMeta);
},
getPageMeta: (pageId: string) => {
return blockSuiteWorkspace.meta.getPageMeta(pageId);
},
}),
[blockSuiteWorkspace]
);
}

View File

@@ -0,0 +1,45 @@
import type { Page, Workspace } from '@blocksuite/store';
import { type Atom, atom, useAtomValue } from 'jotai';
import { useBlockSuiteWorkspacePage } from './use-block-suite-workspace-page';
const weakMap = new WeakMap<Page, Atom<string[]>>();
function getPageReferences(page: Page): string[] {
return Object.values(
page.workspace.indexer.backlink.linkIndexMap[page.id] ?? {}
).flatMap(linkNodes => linkNodes.map(linkNode => linkNode.pageId));
}
const getPageReferencesAtom = (page: Page | null) => {
if (!page) {
return atom([]);
}
if (!weakMap.has(page)) {
const baseAtom = atom<string[]>([]);
baseAtom.onMount = set => {
const disposables = [
page.slots.ready.on(() => {
set(getPageReferences(page));
}),
page.workspace.indexer.backlink.slots.indexUpdated.on(() => {
set(getPageReferences(page));
}),
];
set(getPageReferences(page));
return () => {
disposables.forEach(disposable => disposable.dispose());
};
};
weakMap.set(page, baseAtom);
}
return weakMap.get(page) as Atom<string[]>;
};
export function useBlockSuitePageReferences(
blockSuiteWorkspace: Workspace,
pageId: string
): string[] {
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
return useAtomValue(getPageReferencesAtom(page));
}

View File

@@ -0,0 +1,13 @@
import type { Page, Workspace } from '@blocksuite/store';
import { useMemo } from 'react';
export function useBlockSuiteWorkspaceHelper(blockSuiteWorkspace: Workspace) {
return useMemo(
() => ({
createPage: (pageId?: string): Page => {
return blockSuiteWorkspace.createPage({ id: pageId });
},
}),
[blockSuiteWorkspace]
);
}

View File

@@ -0,0 +1,39 @@
import { assertExists } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import type { Atom } from 'jotai';
import { atom, useAtomValue } from 'jotai';
const weakMap = new WeakMap<Workspace, Map<string, Atom<string>>>();
function getAtom(w: Workspace, pageId: string): Atom<string> {
if (!weakMap.has(w)) {
weakMap.set(w, new Map());
}
const map = weakMap.get(w);
assertExists(map);
if (!map.has(pageId)) {
const baseAtom = atom<string>(w.getPage(pageId)?.meta.title || 'Untitled');
baseAtom.onMount = set => {
const disposable = w.meta.pageMetasUpdated.on(() => {
const page = w.getPage(pageId);
set(page?.meta.title || 'Untitled');
});
return () => {
disposable.dispose();
};
};
map.set(pageId, baseAtom);
return baseAtom;
} else {
return map.get(pageId) as Atom<string>;
}
}
export function useBlockSuiteWorkspacePageTitle(
blockSuiteWorkspace: Workspace,
pageId: string
) {
const titleAtom = getAtom(blockSuiteWorkspace, pageId);
assertExists(titleAtom);
return useAtomValue(titleAtom);
}

View File

@@ -0,0 +1,46 @@
import { DebugLogger } from '@affine/debug';
import { DisposableGroup } from '@blocksuite/global/utils';
import type { Page, Workspace } from '@blocksuite/store';
import { useEffect, useState } from 'react';
const logger = new DebugLogger('use-block-suite-workspace-page');
export function useBlockSuiteWorkspacePage(
blockSuiteWorkspace: Workspace,
pageId: string | null
): Page | null {
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) {
page.load().catch(err => {
logger.error('Failed to load page', err);
});
}
}, [page]);
return page;
}

View File

@@ -0,0 +1,15 @@
import { useCallback, useSyncExternalStore } from 'react';
import type { DataSourceAdapter, Status } from 'y-provider';
type UIStatus =
| Status
| {
type: 'unknown';
};
export function useDataSourceStatus(provider: DataSourceAdapter): UIStatus {
return useSyncExternalStore(
provider.subscribeStatusChange,
useCallback(() => provider.status, [provider])
);
}

View File

@@ -1,6 +1,6 @@
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import type { GetPageInfoById } from '@affine/env/page-info';
import type { Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useCallback, useMemo } from 'react';

View File

@@ -0,0 +1,73 @@
import 'foxact/use-debounced-state';
import { debounce } from 'lodash-es';
import { type RefObject, useEffect, useState } from 'react';
export function useIsTinyScreen({
container,
leftStatic,
leftSlot,
centerDom,
rightSlot,
}: {
container: HTMLElement | null;
leftStatic: RefObject<HTMLElement>;
leftSlot: RefObject<HTMLElement>[];
centerDom: RefObject<HTMLElement>;
rightSlot: RefObject<HTMLElement>[];
}) {
const [isTinyScreen, setIsTinyScreen] = useState(false);
useEffect(() => {
if (!container) {
return;
}
const handleResize = debounce(() => {
if (!centerDom.current) {
return;
}
const leftStaticWidth = leftStatic.current?.clientWidth || 0;
const leftSlotWidth = leftSlot.reduce((accWidth, dom) => {
return accWidth + (dom.current?.clientWidth || 0);
}, 0);
const rightSlotWidth = rightSlot.reduce((accWidth, dom) => {
return accWidth + (dom.current?.clientWidth || 0);
}, 0);
if (!leftSlotWidth && !rightSlotWidth) {
if (isTinyScreen) {
setIsTinyScreen(false);
}
return;
}
const containerRect = container.getBoundingClientRect();
const centerRect = centerDom.current.getBoundingClientRect();
if (
leftStaticWidth + leftSlotWidth + containerRect.left >=
centerRect.left ||
containerRect.right - centerRect.right <= rightSlotWidth
) {
setIsTinyScreen(true);
} else {
setIsTinyScreen(false);
}
}, 100);
handleResize();
const resizeObserver = new ResizeObserver(() => {
handleResize();
});
resizeObserver.observe(container);
return () => {
resizeObserver.unobserve(container);
};
}, [centerDom, isTinyScreen, leftSlot, leftStatic, container, rightSlot]);
return isTinyScreen;
}

View File

@@ -1,5 +1,5 @@
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { type SubscriptionQuery, subscriptionQuery } from '@affine/graphql';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useSelfHosted } from './affine/use-server-config';
import { useQuery } from './use-query';

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,38 @@
import type { WorkspaceMetadata } from '@affine/workspace';
import { workspaceManagerAtom } from '@affine/workspace/atom';
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
import { useWorkspaceBlobObjectUrl } from './use-workspace-blob';
export function useWorkspaceInfo(meta: WorkspaceMetadata) {
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, 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;
}

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