refactor: remove bookmark plugin (#4544)

This commit is contained in:
Alex Yang
2023-09-30 00:48:33 -05:00
committed by GitHub
parent b440c3a820
commit c911806062
23 changed files with 32 additions and 361 deletions

View File

@@ -0,0 +1,244 @@
import { MenuItem, PureMenu } from '@affine/component';
import { MuiClickAwayListener } from '@affine/component';
import type { SerializedBlock } from '@blocksuite/blocks';
import type { BaseBlockModel } from '@blocksuite/store';
import type { Page } from '@blocksuite/store';
import type { VEditor } from '@blocksuite/virgo';
import { useCallback, useEffect, useMemo, useState } from 'react';
type ShortcutMap = {
[key: string]: (e: KeyboardEvent, page: Page) => void;
};
const menuOptions = [
{
id: 'dismiss',
label: 'Dismiss',
},
{
id: 'bookmark',
label: 'Create bookmark',
},
];
function getCurrentNativeRange(selection = window.getSelection()) {
if (!selection) {
return null;
}
if (selection.rangeCount === 0) {
return null;
}
if (selection.rangeCount > 1) {
console.warn('getCurrentRange may be wrong, rangeCount > 1');
}
return selection.getRangeAt(0);
}
const handleEnter = ({
page,
selectedOption,
callback,
}: {
page: Page;
selectedOption: keyof ShortcutMap;
callback: () => void;
}) => {
if (selectedOption === 'dismiss') {
return callback();
}
const native = getCurrentNativeRange();
if (!native) {
return callback();
}
const container = native.startContainer;
const element =
container instanceof Element ? container : container?.parentElement;
const virgo = element?.closest<Element & { virgoEditor: VEditor }>(
'[data-virgo-root]'
)?.virgoEditor;
if (!virgo) {
return callback();
}
const linkInfo = virgo
?.getDeltasByVRange({
index: native.startOffset,
length: 0,
})
.find(delta => delta[0]?.attributes?.link);
if (!linkInfo) {
return;
}
const [, { index, length }] = linkInfo;
const link = linkInfo[0]?.attributes?.link as string;
const model = element?.closest<Element & { model: BaseBlockModel }>(
'[data-block-id]'
)?.model;
if (!model) {
return callback();
}
const parent = page.getParent(model);
if (!parent) {
return callback();
}
const currentBlockIndex = parent.children.indexOf(model);
page.addBlock(
'affine:bookmark',
{ url: link },
parent,
currentBlockIndex + 1
);
virgo?.deleteText({
index,
length,
});
if (model.isEmpty()) {
page.deleteBlock(model);
}
return callback();
};
const shouldShowBookmarkMenu = (pastedBlocks: Record<string, unknown>[]) => {
if (!pastedBlocks.length || pastedBlocks.length > 1) {
return;
}
const [firstBlock] = pastedBlocks as [SerializedBlock];
if (
!firstBlock.text ||
!firstBlock.text.length ||
firstBlock.text.length > 1
) {
return;
}
return !!firstBlock.text[0].attributes?.link;
};
export type BookmarkProps = {
page: Page;
};
export const Bookmark = ({ page }: BookmarkProps) => {
const [anchor, setAnchor] = useState<Range | null>(null);
const [selectedOption, setSelectedOption] = useState<string>(
menuOptions[0].id
);
const shortcutMap = useMemo<ShortcutMap>(
() => ({
ArrowUp: () => {
const curIndex = menuOptions.findIndex(
({ id }) => id === selectedOption
);
if (menuOptions[curIndex - 1]) {
setSelectedOption(menuOptions[curIndex - 1].id);
} else if (curIndex === -1) {
setSelectedOption(menuOptions[0].id);
} else {
setSelectedOption(menuOptions[menuOptions.length - 1].id);
}
},
ArrowDown: () => {
const curIndex = menuOptions.findIndex(
({ id }) => id === selectedOption
);
if (curIndex !== -1 && menuOptions[curIndex + 1]) {
setSelectedOption(menuOptions[curIndex + 1].id);
} else {
setSelectedOption(menuOptions[0].id);
}
},
Enter: () =>
handleEnter({
page,
selectedOption,
callback: () => {
setAnchor(null);
},
}),
Escape: () => {
setAnchor(null);
},
}),
[page, selectedOption]
);
const onKeydown = useCallback(
(e: KeyboardEvent) => {
const shortcut = shortcutMap[e.key];
if (shortcut) {
e.stopPropagation();
e.preventDefault();
shortcut(e, page);
} else {
setAnchor(null);
}
},
[page, shortcutMap]
);
useEffect(() => {
const disposer = page.slots.pasted.on(pastedBlocks => {
if (!shouldShowBookmarkMenu(pastedBlocks)) {
return;
}
window.setTimeout(() => {
setAnchor(getCurrentNativeRange());
}, 100);
});
return () => {
disposer.dispose();
};
}, [onKeydown, page, shortcutMap]);
useEffect(() => {
if (anchor) {
document.addEventListener('keydown', onKeydown, { capture: true });
} else {
// reset status and remove event
setSelectedOption(menuOptions[0].id);
document.removeEventListener('keydown', onKeydown, { capture: true });
}
return () => {
document.removeEventListener('keydown', onKeydown, { capture: true });
};
}, [anchor, onKeydown]);
return anchor ? (
<MuiClickAwayListener
onClickAway={() => {
setAnchor(null);
setSelectedOption('');
}}
>
<div>
<PureMenu open={!!anchor} anchorEl={anchor} placement="bottom-start">
{menuOptions.map(({ id, label }) => {
return (
<MenuItem
key={id}
active={selectedOption === id}
onClick={() => {
handleEnter({
page,
selectedOption: id,
callback: () => {
setAnchor(null);
},
});
}}
disableHover={true}
onMouseEnter={() => {
setSelectedOption(id);
}}
>
{label}
</MenuItem>
);
})}
</PureMenu>
</div>
</MuiClickAwayListener>
) : null;
};

View File

@@ -31,6 +31,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { pageSettingFamily } from '../atoms';
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import { Bookmark } from './bookmark';
import * as styles from './page-detail-editor.css';
import { editorContainer, pluginContainer } from './page-detail-editor.css';
import { TrashButtonGroup } from './pure/trash-button-group';
@@ -139,6 +140,7 @@ const EditorWrapper = memo(function EditorWrapper({
)}
/>
{meta.trash && <TrashButtonGroup />}
<Bookmark page={page} />
</>
);
});

View File

@@ -26,7 +26,6 @@
"main": "./dist/main.js",
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/bookmark-plugin": "workspace:*",
"@affine/copilot-plugin": "workspace:*",
"@affine/env": "workspace:*",
"@affine/hello-world-plugin": "workspace:*",

View File

@@ -32,7 +32,6 @@ export const config = () => {
resolve(electronDir, './src/main/index.ts'),
resolve(electronDir, './src/preload/index.ts'),
resolve(electronDir, './src/helper/index.ts'),
resolve(electronDir, './src/worker/plugin.ts'),
],
entryNames: '[dir]',
outdir: resolve(electronDir, './dist'),

View File

@@ -64,7 +64,6 @@ export const registerHandlers = () => {
ipcMain.handle(chan, async (e, ...args) => {
const start = performance.now();
try {
// @ts-expect-error - TODO: fix this
const result = await handler(e, ...args);
logger.info(
'[ipc-api]',

View File

@@ -9,7 +9,6 @@ import { registerHandlers } from './handlers';
import { ensureHelperProcess } from './helper-process';
import { logger } from './logger';
import { restoreOrCreateWindow } from './main-window';
import { registerPlugin } from './plugin';
import { registerProtocol } from './protocol';
import { registerUpdater } from './updater';
@@ -59,7 +58,6 @@ setupDeepLink(app);
app
.whenReady()
.then(registerProtocol)
.then(registerPlugin)
.then(registerHandlers)
.then(registerEvents)
.then(ensureHelperProcess)

View File

@@ -1,57 +0,0 @@
import { join, resolve } from 'node:path';
import { Worker } from 'node:worker_threads';
import { logger, pluginLogger } from '@affine/electron/main/logger';
import { AsyncCall } from 'async-call-rpc';
import { ipcMain } from 'electron';
import { readFile } from 'fs/promises';
import { MessageEventChannel } from '../shared/utils';
const builtInPlugins = ['bookmark'];
declare global {
// fixme(himself65):
// remove this when bookmark block plugin is migrated to plugin-infra
// eslint-disable-next-line no-var
var asyncCall: Record<string, (...args: any) => PromiseLike<any>>;
}
export async function registerPlugin() {
logger.info('import plugin manager');
const asyncCall = AsyncCall<
Record<string, (...args: any) => PromiseLike<any>>
>(
{
log: (...args: any[]) => {
pluginLogger.log(...args);
},
},
{
channel: new MessageEventChannel(
new Worker(resolve(__dirname, './worker.js'), {})
),
}
);
globalThis.asyncCall = asyncCall;
await Promise.all(
builtInPlugins.map(async plugin => {
const pluginPackageJsonPath = join(
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
`./${plugin}/package.json`
);
logger.info(`${plugin} plugin path:`, pluginPackageJsonPath);
const packageJson = JSON.parse(
await readFile(pluginPackageJsonPath, 'utf-8')
);
console.log('packageJson', packageJson);
const serverCommand: string[] = packageJson.affinePlugin.serverCommand;
serverCommand.forEach(command => {
ipcMain.handle(command, async (_, ...args) => {
logger.info(`plugin ${plugin} called`);
return asyncCall[command](...args);
});
});
})
);
}

View File

@@ -1,4 +1,5 @@
import { app, BrowserWindow, nativeTheme } from 'electron';
import { getLinkPreview } from 'link-preview-js';
import { isMacOS } from '../../shared/utils';
import type { NamespaceHandlers } from '../type';
@@ -43,12 +44,30 @@ export const uiHandlers = {
getGoogleOauthCode: async () => {
return getGoogleOauthCode();
},
/**
* @deprecated Remove this when bookmark block plugin is migrated to plugin-infra
*/
getBookmarkDataByLink: async (_, link: string) => {
return globalThis.asyncCall[
'com.blocksuite.bookmark-block.get-bookmark-data-by-link'
](link);
const previewData = (await getLinkPreview(link, {
timeout: 6000,
headers: {
'user-agent': 'googlebot',
},
followRedirects: 'follow',
}).catch(() => {
return {
title: '',
siteName: '',
description: '',
images: [],
videos: [],
contentType: `text/html`,
favicons: [],
};
})) as any;
return {
title: previewData.title,
description: previewData.description,
icon: previewData.favicons[0],
image: previewData.images[0],
};
},
} satisfies NamespaceHandlers;

View File

@@ -1,52 +0,0 @@
import { join, resolve } from 'node:path';
import { parentPort } from 'node:worker_threads';
import type { ServerContext } from '@affine/sdk/server';
import { AsyncCall } from 'async-call-rpc';
import { MessageEventChannel } from '../shared/utils';
if (!parentPort) {
throw new Error('parentPort is null');
}
const commandProxy: Record<string, (...args: any[]) => Promise<any>> = {};
parentPort.start();
const mainThread = AsyncCall<{
log: (...args: any[]) => Promise<void>;
}>(commandProxy, {
channel: new MessageEventChannel(parentPort),
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
globalThis.console.log = mainThread.log;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
globalThis.console.error = mainThread.log;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
globalThis.console.info = mainThread.log;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
globalThis.console.debug = mainThread.log;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
globalThis.console.warn = mainThread.log;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const bookmarkPluginModule = require(
join(
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
'./bookmark/index.cjs'
)
);
const serverContext: ServerContext = {
registerCommand: (command, fn) => {
console.log('register command', command);
commandProxy[command] = fn;
},
unregisterCommand: command => {
console.log('unregister command', command);
delete commandProxy[command];
},
};
bookmarkPluginModule.entry(serverContext);