refactor(editor): refactor linkPreviewer as an extension and remove bookmark service (#9754)

[BS-2427](https://linear.app/affine-design/issue/BS-2427/移除-bookmark-block-service) [BS-2418](https://linear.app/affine-design/issue/BS-2418/linkpreviewer-重构成插件)
This commit is contained in:
donteatfriedrice
2025-01-20 04:18:00 +00:00
parent cc2958203b
commit 4bd43a698c
17 changed files with 42 additions and 105 deletions
@@ -10,15 +10,11 @@ import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import type { BookmarkBlockService } from './bookmark-service.js';
import { refreshBookmarkUrlData } from './utils.js'; import { refreshBookmarkUrlData } from './utils.js';
export const BOOKMARK_MIN_WIDTH = 450; export const BOOKMARK_MIN_WIDTH = 450;
export class BookmarkBlockComponent extends CaptionedBlockComponent< export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBlockModel> {
BookmarkBlockModel,
BookmarkBlockService
> {
private _fetchAbortController?: AbortController; private _fetchAbortController?: AbortController;
blockDraggable = true; blockDraggable = true;
@@ -1,16 +0,0 @@
import { LinkPreviewer } from '@blocksuite/affine-block-embed';
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockService } from '@blocksuite/block-std';
export class BookmarkBlockService extends BlockService {
static override readonly flavour = BookmarkBlockSchema.model.flavour;
private static readonly linkPreviewer = new LinkPreviewer();
static setLinkPreviewEndpoint =
BookmarkBlockService.linkPreviewer.setEndpoint;
queryUrlData = (url: string, signal?: AbortSignal) => {
return BookmarkBlockService.linkPreviewer.query(url, signal);
};
}
@@ -7,12 +7,10 @@ import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { BookmarkBlockAdapterExtensions } from './adapters/extension.js'; import { BookmarkBlockAdapterExtensions } from './adapters/extension.js';
import { BookmarkBlockService } from './bookmark-service.js';
import { commands } from './commands/index.js'; import { commands } from './commands/index.js';
export const BookmarkBlockSpec: ExtensionType[] = [ export const BookmarkBlockSpec: ExtensionType[] = [
FlavourExtension('affine:bookmark'), FlavourExtension('affine:bookmark'),
BookmarkBlockService,
CommandExtension(commands), CommandExtension(commands),
BlockViewExtension('affine:bookmark', model => { BlockViewExtension('affine:bookmark', model => {
return model.parent?.flavour === 'affine:surface' return model.parent?.flavour === 'affine:surface'
@@ -1,6 +1,5 @@
import { BookmarkBlockComponent } from './bookmark-block'; import { BookmarkBlockComponent } from './bookmark-block';
import { BookmarkEdgelessBlockComponent } from './bookmark-edgeless-block'; import { BookmarkEdgelessBlockComponent } from './bookmark-edgeless-block';
import type { BookmarkBlockService } from './bookmark-service';
import type { insertBookmarkCommand } from './commands/insert-bookmark'; import type { insertBookmarkCommand } from './commands/insert-bookmark';
import type { insertLinkByQuickSearchCommand } from './commands/insert-link-by-quick-search'; import type { insertLinkByQuickSearchCommand } from './commands/insert-link-by-quick-search';
import { BookmarkCard } from './components/bookmark-card'; import { BookmarkCard } from './components/bookmark-card';
@@ -32,8 +31,5 @@ declare global {
insertBookmark: typeof insertBookmarkCommand; insertBookmark: typeof insertBookmarkCommand;
insertLinkByQuickSearch: typeof insertLinkByQuickSearchCommand; insertLinkByQuickSearch: typeof insertLinkByQuickSearchCommand;
} }
interface BlockServices {
'affine:bookmark': BookmarkBlockService;
}
} }
} }
@@ -1,5 +1,4 @@
export * from './adapters'; export * from './adapters';
export * from './bookmark-block'; export * from './bookmark-block';
export * from './bookmark-service';
export * from './bookmark-spec'; export * from './bookmark-spec';
export * from './components'; export * from './components';
@@ -1,5 +1,5 @@
import { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils'; import { isAbortError } from '@blocksuite/affine-shared/utils';
import { assertExists } from '@blocksuite/global/utils';
import type { BookmarkBlockComponent } from './bookmark-block.js'; import type { BookmarkBlockComponent } from './bookmark-block.js';
@@ -15,10 +15,8 @@ export async function refreshBookmarkUrlData(
try { try {
bookmarkElement.loading = true; bookmarkElement.loading = true;
const queryUrlData = bookmarkElement.service?.queryUrlData; const linkPreviewer = bookmarkElement.doc.get(LinkPreviewerService);
assertExists(queryUrlData); const bookmarkUrlData = await linkPreviewer.query(
const bookmarkUrlData = await queryUrlData(
bookmarkElement.model.url, bookmarkElement.model.url,
signal signal
); );
@@ -3,21 +3,18 @@ import {
type EmbedGithubModel, type EmbedGithubModel,
EmbedGithubStyles, EmbedGithubStyles,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services'; import {
EmbedOptionProvider,
LinkPreviewerService,
} from '@blocksuite/affine-shared/services';
import { BlockService } from '@blocksuite/block-std'; import { BlockService } from '@blocksuite/block-std';
import { LinkPreviewer } from '../common/link-previewer.js';
import { githubUrlRegex } from './embed-github-model.js'; import { githubUrlRegex } from './embed-github-model.js';
import { queryEmbedGithubApiData, queryEmbedGithubData } from './utils.js'; import { queryEmbedGithubApiData, queryEmbedGithubData } from './utils.js';
export class EmbedGithubBlockService extends BlockService { export class EmbedGithubBlockService extends BlockService {
static override readonly flavour = EmbedGithubBlockSchema.model.flavour; static override readonly flavour = EmbedGithubBlockSchema.model.flavour;
private static readonly linkPreviewer = new LinkPreviewer();
static setLinkPreviewEndpoint =
EmbedGithubBlockService.linkPreviewer.setEndpoint;
queryApiData = (embedGithubModel: EmbedGithubModel, signal?: AbortSignal) => { queryApiData = (embedGithubModel: EmbedGithubModel, signal?: AbortSignal) => {
return queryEmbedGithubApiData(embedGithubModel, signal); return queryEmbedGithubApiData(embedGithubModel, signal);
}; };
@@ -25,7 +22,7 @@ export class EmbedGithubBlockService extends BlockService {
queryUrlData = (embedGithubModel: EmbedGithubModel, signal?: AbortSignal) => { queryUrlData = (embedGithubModel: EmbedGithubModel, signal?: AbortSignal) => {
return queryEmbedGithubData( return queryEmbedGithubData(
embedGithubModel, embedGithubModel,
EmbedGithubBlockService.linkPreviewer, this.doc.get(LinkPreviewerService),
signal signal
); );
}; };
@@ -2,11 +2,11 @@ import type {
EmbedGithubBlockUrlData, EmbedGithubBlockUrlData,
EmbedGithubModel, EmbedGithubModel,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import type { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils'; import { isAbortError } from '@blocksuite/affine-shared/utils';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { nothing } from 'lit'; import { nothing } from 'lit';
import type { LinkPreviewer } from '../common/link-previewer.js';
import type { EmbedGithubBlockComponent } from './embed-github-block.js'; import type { EmbedGithubBlockComponent } from './embed-github-block.js';
import { import {
GithubIssueClosedFailureIcon, GithubIssueClosedFailureIcon,
@@ -20,7 +20,7 @@ import {
export async function queryEmbedGithubData( export async function queryEmbedGithubData(
embedGithubModel: EmbedGithubModel, embedGithubModel: EmbedGithubModel,
linkPreviewer: LinkPreviewer, linkPreviewer: LinkPreviewerService,
signal?: AbortSignal signal?: AbortSignal
): Promise<Partial<EmbedGithubBlockUrlData>> { ): Promise<Partial<EmbedGithubBlockUrlData>> {
const [githubApiData, openGraphData] = await Promise.all([ const [githubApiData, openGraphData] = await Promise.all([
@@ -6,18 +6,12 @@ import {
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services'; import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
import { BlockService } from '@blocksuite/block-std'; import { BlockService } from '@blocksuite/block-std';
import { LinkPreviewer } from '../common/link-previewer.js';
import { loomUrlRegex } from './embed-loom-model.js'; import { loomUrlRegex } from './embed-loom-model.js';
import { queryEmbedLoomData } from './utils.js'; import { queryEmbedLoomData } from './utils.js';
export class EmbedLoomBlockService extends BlockService { export class EmbedLoomBlockService extends BlockService {
static override readonly flavour = EmbedLoomBlockSchema.model.flavour; static override readonly flavour = EmbedLoomBlockSchema.model.flavour;
private static readonly linkPreviewer = new LinkPreviewer();
static setLinkPreviewEndpoint =
EmbedLoomBlockService.linkPreviewer.setEndpoint;
queryUrlData = (embedLoomModel: EmbedLoomModel, signal?: AbortSignal) => { queryUrlData = (embedLoomModel: EmbedLoomModel, signal?: AbortSignal) => {
return queryEmbedLoomData(embedLoomModel, signal); return queryEmbedLoomData(embedLoomModel, signal);
}; };
@@ -3,28 +3,25 @@ import {
type EmbedYoutubeModel, type EmbedYoutubeModel,
EmbedYoutubeStyles, EmbedYoutubeStyles,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services'; import {
EmbedOptionProvider,
LinkPreviewerService,
} from '@blocksuite/affine-shared/services';
import { BlockService } from '@blocksuite/block-std'; import { BlockService } from '@blocksuite/block-std';
import { LinkPreviewer } from '../common/link-previewer.js';
import { youtubeUrlRegex } from './embed-youtube-model.js'; import { youtubeUrlRegex } from './embed-youtube-model.js';
import { queryEmbedYoutubeData } from './utils.js'; import { queryEmbedYoutubeData } from './utils.js';
export class EmbedYoutubeBlockService extends BlockService { export class EmbedYoutubeBlockService extends BlockService {
static override readonly flavour = EmbedYoutubeBlockSchema.model.flavour; static override readonly flavour = EmbedYoutubeBlockSchema.model.flavour;
private static readonly linkPreviewer = new LinkPreviewer();
static setLinkPreviewEndpoint =
EmbedYoutubeBlockService.linkPreviewer.setEndpoint;
queryUrlData = ( queryUrlData = (
embedYoutubeModel: EmbedYoutubeModel, embedYoutubeModel: EmbedYoutubeModel,
signal?: AbortSignal signal?: AbortSignal
) => { ) => {
return queryEmbedYoutubeData( return queryEmbedYoutubeData(
embedYoutubeModel, embedYoutubeModel,
EmbedYoutubeBlockService.linkPreviewer, this.doc.get(LinkPreviewerService),
signal signal
); );
}; };
@@ -2,15 +2,15 @@ import type {
EmbedYoutubeBlockUrlData, EmbedYoutubeBlockUrlData,
EmbedYoutubeModel, EmbedYoutubeModel,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import type { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils'; import { isAbortError } from '@blocksuite/affine-shared/utils';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { LinkPreviewer } from '../common/link-previewer.js';
import type { EmbedYoutubeBlockComponent } from './embed-youtube-block.js'; import type { EmbedYoutubeBlockComponent } from './embed-youtube-block.js';
export async function queryEmbedYoutubeData( export async function queryEmbedYoutubeData(
embedYoutubeModel: EmbedYoutubeModel, embedYoutubeModel: EmbedYoutubeModel,
linkPreviewer: LinkPreviewer, linkPreviewer: LinkPreviewerService,
signal?: AbortSignal signal?: AbortSignal
): Promise<Partial<EmbedYoutubeBlockUrlData>> { ): Promise<Partial<EmbedYoutubeBlockUrlData>> {
const url = embedYoutubeModel.url; const url = embedYoutubeModel.url;
@@ -23,10 +23,6 @@ export { createEmbedBlockMarkdownAdapterMatcher } from './common/adapters/markdo
export { createEmbedBlockPlainTextAdapterMatcher } from './common/adapters/plain-text'; export { createEmbedBlockPlainTextAdapterMatcher } from './common/adapters/plain-text';
export { EmbedBlockComponent } from './common/embed-block-element'; export { EmbedBlockComponent } from './common/embed-block-element';
export { insertEmbedCard } from './common/insert-embed-card.js'; export { insertEmbedCard } from './common/insert-embed-card.js';
export {
LinkPreviewer,
type LinkPreviewResponseData,
} from './common/link-previewer.js';
export * from './common/render-linked-doc'; export * from './common/render-linked-doc';
export { toEdgelessEmbedBlock } from './common/to-edgeless-embed-block'; export { toEdgelessEmbedBlock } from './common/to-edgeless-embed-block';
export * from './common/utils'; export * from './common/utils';
@@ -7,6 +7,7 @@ export * from './embed-option-service';
export * from './feature-flag-service'; export * from './feature-flag-service';
export * from './font-loader'; export * from './font-loader';
export * from './generate-url-service'; export * from './generate-url-service';
export * from './link-previewer-service';
export * from './native-clipboard-service'; export * from './native-clipboard-service';
export * from './notification-service'; export * from './notification-service';
export * from './open-doc-config'; export * from './open-doc-config';
@@ -1,7 +1,9 @@
import type { LinkPreviewData } from '@blocksuite/affine-model'; import type { LinkPreviewData } from '@blocksuite/affine-model';
import { DEFAULT_LINK_PREVIEW_ENDPOINT } from '@blocksuite/affine-shared/consts';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { StoreExtension } from '@blocksuite/store';
import { DEFAULT_LINK_PREVIEW_ENDPOINT } from '../consts';
import { isAbortError } from '../utils/is-abort-error';
export type LinkPreviewResponseData = { export type LinkPreviewResponseData = {
url: string; url: string;
@@ -16,7 +18,9 @@ export type LinkPreviewResponseData = {
favicons?: string[]; favicons?: string[];
}; };
export class LinkPreviewer { export class LinkPreviewerService extends StoreExtension {
static override key = 'link-previewer';
private _endpoint = DEFAULT_LINK_PREVIEW_ENDPOINT; private _endpoint = DEFAULT_LINK_PREVIEW_ENDPOINT;
query = async ( query = async (
@@ -77,23 +81,19 @@ export class LinkPreviewer {
const data: LinkPreviewResponseData = await response.json(); const data: LinkPreviewResponseData = await response.json();
return { return {
title: data.title ? this._getStringFromHTML(data.title) : null, title: data.title ?? null,
description: data.description description: data.description ?? null,
? this._getStringFromHTML(data.description)
: null,
icon: data.favicons?.[0], icon: data.favicons?.[0],
image: data.images?.[0], image: data.images?.[0],
}; };
} }
}; };
get endpoint() {
return this._endpoint;
}
setEndpoint = (endpoint: string) => { setEndpoint = (endpoint: string) => {
this._endpoint = endpoint; this._endpoint = endpoint;
}; };
private _getStringFromHTML(html: string) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent;
}
} }
+2
View File
@@ -40,6 +40,7 @@ import {
EditPropsStore, EditPropsStore,
FeatureFlagService, FeatureFlagService,
FontLoaderService, FontLoaderService,
LinkPreviewerService,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { import {
BlockSelectionExtension, BlockSelectionExtension,
@@ -99,4 +100,5 @@ export const StoreExtensions: ExtensionType[] = [
HighlightSelectionExtension, HighlightSelectionExtension,
ImageSelectionExtension, ImageSelectionExtension,
DatabaseSelectionExtension, DatabaseSelectionExtension,
LinkPreviewerService,
]; ];
@@ -1,13 +1,10 @@
import { useRefEffect } from '@affine/component'; import { useRefEffect } from '@affine/component';
import { EditorLoading } from '@affine/component/page-detail-skeleton'; import { EditorLoading } from '@affine/component/page-detail-skeleton';
import { import {
BookmarkBlockService,
customImageProxyMiddleware, customImageProxyMiddleware,
type DocMode, type DocMode,
EmbedGithubBlockService,
EmbedLoomBlockService,
EmbedYoutubeBlockService,
ImageBlockService, ImageBlockService,
LinkPreviewerService,
} from '@blocksuite/affine/blocks'; } from '@blocksuite/affine/blocks';
import { DisposableGroup } from '@blocksuite/affine/global/utils'; import { DisposableGroup } from '@blocksuite/affine/global/utils';
import type { AffineEditorContainer } from '@blocksuite/affine/presets'; import type { AffineEditorContainer } from '@blocksuite/affine/presets';
@@ -71,19 +68,9 @@ const BlockSuiteEditorImpl = ({
); );
ImageBlockService.setImageProxyURL(BUILD_CONFIG.imageProxyUrl); ImageBlockService.setImageProxyURL(BUILD_CONFIG.imageProxyUrl);
// provide link preview endpoint to blocksuite editor.host?.doc
BookmarkBlockService.setLinkPreviewEndpoint( .get(LinkPreviewerService)
BUILD_CONFIG.linkPreviewUrl .setEndpoint(BUILD_CONFIG.linkPreviewUrl);
);
EmbedGithubBlockService.setLinkPreviewEndpoint(
BUILD_CONFIG.linkPreviewUrl
);
EmbedYoutubeBlockService.setLinkPreviewEndpoint(
BUILD_CONFIG.linkPreviewUrl
);
EmbedLoomBlockService.setLinkPreviewEndpoint(
BUILD_CONFIG.linkPreviewUrl
);
return editor.host?.updateComplete; return editor.host?.updateComplete;
}) })
@@ -22,12 +22,9 @@ import { ViewService } from '@affine/core/modules/workbench/services/view';
import { WorkspaceService } from '@affine/core/modules/workspace'; import { WorkspaceService } from '@affine/core/modules/workspace';
import { i18nTime } from '@affine/i18n'; import { i18nTime } from '@affine/i18n';
import { import {
BookmarkBlockService,
customImageProxyMiddleware, customImageProxyMiddleware,
EmbedGithubBlockService,
EmbedLoomBlockService,
EmbedYoutubeBlockService,
ImageBlockService, ImageBlockService,
LinkPreviewerService,
RefNodeSlotsProvider, RefNodeSlotsProvider,
} from '@blocksuite/affine/blocks'; } from '@blocksuite/affine/blocks';
import { DisposableGroup } from '@blocksuite/affine/global/utils'; import { DisposableGroup } from '@blocksuite/affine/global/utils';
@@ -159,14 +156,9 @@ const DetailPageImpl = () => {
ImageBlockService.setImageProxyURL(BUILD_CONFIG.imageProxyUrl); ImageBlockService.setImageProxyURL(BUILD_CONFIG.imageProxyUrl);
// provide link preview endpoint to blocksuite // provide link preview endpoint to blocksuite
BookmarkBlockService.setLinkPreviewEndpoint(BUILD_CONFIG.linkPreviewUrl); editorHost?.doc
EmbedGithubBlockService.setLinkPreviewEndpoint( .get(LinkPreviewerService)
BUILD_CONFIG.linkPreviewUrl .setEndpoint(BUILD_CONFIG.linkPreviewUrl);
);
EmbedYoutubeBlockService.setLinkPreviewEndpoint(
BUILD_CONFIG.linkPreviewUrl
);
EmbedLoomBlockService.setLinkPreviewEndpoint(BUILD_CONFIG.linkPreviewUrl);
// provide page mode and updated date to blocksuite // provide page mode and updated date to blocksuite
const refNodeService = editorHost?.std.getOptional(RefNodeSlotsProvider); const refNodeService = editorHost?.std.getOptional(RefNodeSlotsProvider);