diff --git a/blocksuite/affine/blocks/bookmark/src/bookmark-block.ts b/blocksuite/affine/blocks/bookmark/src/bookmark-block.ts index 22f862a99d..a9c619b446 100644 --- a/blocksuite/affine/blocks/bookmark/src/bookmark-block.ts +++ b/blocksuite/affine/blocks/bookmark/src/bookmark-block.ts @@ -2,10 +2,16 @@ import { CaptionedBlockComponent, SelectedStyle, } from '@blocksuite/affine-components/caption'; -import type { BookmarkBlockModel } from '@blocksuite/affine-model'; -import { DocModeProvider } from '@blocksuite/affine-shared/services'; +import type { + BookmarkBlockModel, + LinkPreviewData, +} from '@blocksuite/affine-model'; +import { + DocModeProvider, + LinkPreviewerService, +} from '@blocksuite/affine-shared/services'; import { BlockSelection } from '@blocksuite/std'; -import { computed, type ReadonlySignal } from '@preact/signals-core'; +import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; import { html } from 'lit'; import { property, query } from 'lit/decorators.js'; import { type ClassInfo, classMap } from 'lit/directives/class-map.js'; @@ -28,6 +34,62 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent; + /** + * @description Local link preview data + * When the doc is in readonly mode, and the link preview data are not provided (stored in the block model), + * We will use the local link preview data fetched from the link previewer service to render the block. + */ + private readonly _localLinkPreview$ = signal({ + icon: null, + title: null, + description: null, + image: null, + }); + + /** + * @description Link preview data for actual rendering + * When the doc is not in readonly mode, and the link preview data are provided (stored in the block model), + * We will use the model props to render the block. + * Otherwise, we will use the local link preview data to render the block. + */ + linkPreview$ = computed(() => { + const modelProps = this.model.props; + const local = this._localLinkPreview$.value; + return { + icon: modelProps.icon$.value ?? local.icon ?? null, + title: modelProps.title$.value ?? local.title ?? null, + description: modelProps.description$.value ?? local.description ?? null, + image: modelProps.image$.value ?? local.image ?? null, + }; + }); + + private readonly _updateLocalLinkPreview = () => { + // cancel any inflight request + this._fetchAbortController?.abort(); + this._fetchAbortController = new AbortController(); + + this.loading = true; + this.error = false; + + this.std.store + .get(LinkPreviewerService) + .query(this.model.props.url, this._fetchAbortController.signal) + .then(data => { + this._localLinkPreview$.value = { + icon: data.icon ?? null, + title: data.title ?? null, + description: data.description ?? null, + image: data.image ?? null, + }; + }) + .catch(() => { + this.error = true; + }) + .finally(() => { + this.loading = false; + }); + }; + selectBlock = () => { const selectionManager = this.std.selection; const blockSelection = selectionManager.create(BlockSelection, { @@ -57,17 +119,30 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent { + event.stopPropagation(); + + if (this.model.parent?.flavour !== 'affine:surface' && !this.doc.readonly) { + this.selectBlock(); + } + }; + + handleDoubleClick = (event: MouseEvent) => { + event.stopPropagation(); + this.open(); + }; + private readonly _renderCitationView = () => { - const { title, description, url, icon, footnoteIdentifier } = - this.model.props; + const { url, footnoteIdentifier } = this.model.props; + const { icon, title, description } = this.linkPreview$.value; return html` `; @@ -97,10 +172,17 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent