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

View File

@@ -1,99 +0,0 @@
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';
export type LinkPreviewResponseData = {
url: string;
title?: string;
siteName?: string;
description?: string;
images?: string[];
mediaType?: string;
contentType?: string;
charset?: string;
videos?: string[];
favicons?: string[];
};
export class LinkPreviewer {
private _endpoint = DEFAULT_LINK_PREVIEW_ENDPOINT;
query = async (
url: string,
signal?: AbortSignal
): Promise<Partial<LinkPreviewData>> => {
if (
(url.startsWith('https://x.com/') ||
url.startsWith('https://www.x.com/') ||
url.startsWith('https://www.twitter.com/') ||
url.startsWith('https://twitter.com/')) &&
url.includes('/status/')
) {
// use api.fxtwitter.com
url =
'https://api.fxtwitter.com/status/' + /\/status\/(.*)/.exec(url)?.[1];
try {
const { tweet } = await fetch(url, { signal }).then(res => res.json());
return {
title: tweet.author.name,
icon: tweet.author.avatar_url,
description: tweet.text,
image: tweet.media?.photos?.[0].url || tweet.author.banner_url,
};
} catch (e) {
console.error(`Failed to fetch tweet: ${url}`);
console.error(e);
return {};
}
} else {
const response = await fetch(this._endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
}),
signal,
})
.then(r => {
if (!r || !r.ok) {
throw new BlockSuiteError(
ErrorCode.DefaultRuntimeError,
`Failed to fetch link preview: ${url}`
);
}
return r;
})
.catch(err => {
if (isAbortError(err)) return null;
console.error(`Failed to fetch link preview: ${url}`);
console.error(err);
return null;
});
if (!response) return {};
const data: LinkPreviewResponseData = await response.json();
return {
title: data.title ? this._getStringFromHTML(data.title) : null,
description: data.description
? this._getStringFromHTML(data.description)
: null,
icon: data.favicons?.[0],
image: data.images?.[0],
};
}
};
setEndpoint = (endpoint: string) => {
this._endpoint = endpoint;
};
private _getStringFromHTML(html: string) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent;
}
}

View File

@@ -3,21 +3,18 @@ import {
type EmbedGithubModel,
EmbedGithubStyles,
} 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 { LinkPreviewer } from '../common/link-previewer.js';
import { githubUrlRegex } from './embed-github-model.js';
import { queryEmbedGithubApiData, queryEmbedGithubData } from './utils.js';
export class EmbedGithubBlockService extends BlockService {
static override readonly flavour = EmbedGithubBlockSchema.model.flavour;
private static readonly linkPreviewer = new LinkPreviewer();
static setLinkPreviewEndpoint =
EmbedGithubBlockService.linkPreviewer.setEndpoint;
queryApiData = (embedGithubModel: EmbedGithubModel, signal?: AbortSignal) => {
return queryEmbedGithubApiData(embedGithubModel, signal);
};
@@ -25,7 +22,7 @@ export class EmbedGithubBlockService extends BlockService {
queryUrlData = (embedGithubModel: EmbedGithubModel, signal?: AbortSignal) => {
return queryEmbedGithubData(
embedGithubModel,
EmbedGithubBlockService.linkPreviewer,
this.doc.get(LinkPreviewerService),
signal
);
};

View File

@@ -2,11 +2,11 @@ import type {
EmbedGithubBlockUrlData,
EmbedGithubModel,
} from '@blocksuite/affine-model';
import type { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import { assertExists } from '@blocksuite/global/utils';
import { nothing } from 'lit';
import type { LinkPreviewer } from '../common/link-previewer.js';
import type { EmbedGithubBlockComponent } from './embed-github-block.js';
import {
GithubIssueClosedFailureIcon,
@@ -20,7 +20,7 @@ import {
export async function queryEmbedGithubData(
embedGithubModel: EmbedGithubModel,
linkPreviewer: LinkPreviewer,
linkPreviewer: LinkPreviewerService,
signal?: AbortSignal
): Promise<Partial<EmbedGithubBlockUrlData>> {
const [githubApiData, openGraphData] = await Promise.all([

View File

@@ -6,18 +6,12 @@ import {
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
import { BlockService } from '@blocksuite/block-std';
import { LinkPreviewer } from '../common/link-previewer.js';
import { loomUrlRegex } from './embed-loom-model.js';
import { queryEmbedLoomData } from './utils.js';
export class EmbedLoomBlockService extends BlockService {
static override readonly flavour = EmbedLoomBlockSchema.model.flavour;
private static readonly linkPreviewer = new LinkPreviewer();
static setLinkPreviewEndpoint =
EmbedLoomBlockService.linkPreviewer.setEndpoint;
queryUrlData = (embedLoomModel: EmbedLoomModel, signal?: AbortSignal) => {
return queryEmbedLoomData(embedLoomModel, signal);
};

View File

@@ -3,28 +3,25 @@ import {
type EmbedYoutubeModel,
EmbedYoutubeStyles,
} 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 { LinkPreviewer } from '../common/link-previewer.js';
import { youtubeUrlRegex } from './embed-youtube-model.js';
import { queryEmbedYoutubeData } from './utils.js';
export class EmbedYoutubeBlockService extends BlockService {
static override readonly flavour = EmbedYoutubeBlockSchema.model.flavour;
private static readonly linkPreviewer = new LinkPreviewer();
static setLinkPreviewEndpoint =
EmbedYoutubeBlockService.linkPreviewer.setEndpoint;
queryUrlData = (
embedYoutubeModel: EmbedYoutubeModel,
signal?: AbortSignal
) => {
return queryEmbedYoutubeData(
embedYoutubeModel,
EmbedYoutubeBlockService.linkPreviewer,
this.doc.get(LinkPreviewerService),
signal
);
};

View File

@@ -2,15 +2,15 @@ import type {
EmbedYoutubeBlockUrlData,
EmbedYoutubeModel,
} from '@blocksuite/affine-model';
import type { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import { assertExists } from '@blocksuite/global/utils';
import type { LinkPreviewer } from '../common/link-previewer.js';
import type { EmbedYoutubeBlockComponent } from './embed-youtube-block.js';
export async function queryEmbedYoutubeData(
embedYoutubeModel: EmbedYoutubeModel,
linkPreviewer: LinkPreviewer,
linkPreviewer: LinkPreviewerService,
signal?: AbortSignal
): Promise<Partial<EmbedYoutubeBlockUrlData>> {
const url = embedYoutubeModel.url;

View File

@@ -23,10 +23,6 @@ export { createEmbedBlockMarkdownAdapterMatcher } from './common/adapters/markdo
export { createEmbedBlockPlainTextAdapterMatcher } from './common/adapters/plain-text';
export { EmbedBlockComponent } from './common/embed-block-element';
export { insertEmbedCard } from './common/insert-embed-card.js';
export {
LinkPreviewer,
type LinkPreviewResponseData,
} from './common/link-previewer.js';
export * from './common/render-linked-doc';
export { toEdgelessEmbedBlock } from './common/to-edgeless-embed-block';
export * from './common/utils';