diff --git a/blocksuite/affine/components/src/rich-text/inline/adapters/markdown/markdown-inline.ts b/blocksuite/affine/components/src/rich-text/inline/adapters/markdown/markdown-inline.ts index c82ca5f25..7932f0ba0 100644 --- a/blocksuite/affine/components/src/rich-text/inline/adapters/markdown/markdown-inline.ts +++ b/blocksuite/affine/components/src/rich-text/inline/adapters/markdown/markdown-inline.ts @@ -173,7 +173,7 @@ export const markdownFootnoteReferenceToDeltaMatcher = }; return [{ insert: ' ', attributes: { footnote } }]; } catch (error) { - console.error('Error parsing footnote reference', error); + console.warn('Error parsing footnote reference', error); return []; } }, diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts index 865acd4cb..7cdab0f83 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts @@ -15,6 +15,7 @@ import { ZERO_WIDTH_NON_JOINER, ZERO_WIDTH_SPACE, } from '@blocksuite/inline'; +import { shift } from '@floating-ui/dom'; import { baseTheme } from '@toeverything/theme'; import { css, html, nothing, unsafeCSS } from 'lit'; import { property } from 'lit/decorators.js'; @@ -129,6 +130,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) { referenceElement: this, placement: 'top', autoUpdate: true, + middleware: [shift()], }, }; }, diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts index 66193f7c2..4a06f183b 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts @@ -26,9 +26,11 @@ export class FootNotePopupChip extends LitElement { color: ${unsafeCSSVarV2('icon/primary')}; border-radius: 4px; - svg { + svg, + object { width: 16px; height: 16px; + fill: ${unsafeCSSVarV2('icon/primary')}; } } @@ -59,7 +61,7 @@ export class FootNotePopupChip extends LitElement { ${this.prefixIcon} ` : nothing} - + ${this.suffixIcon ? html`
${this.suffixIcon} @@ -78,6 +80,9 @@ export class FootNotePopupChip extends LitElement { @property({ attribute: false }) accessor suffixIcon: TemplateResult | undefined = undefined; + @property({ attribute: false }) + accessor tooltip: string = ''; + @property({ attribute: false }) accessor onClick: (() => void) | undefined = undefined; diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts index f0f948329..3ea015660 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts @@ -1,16 +1,26 @@ -import type { FootNote } from '@blocksuite/affine-model'; -import { DocDisplayMetaProvider } from '@blocksuite/affine-shared/services'; +import { ColorScheme, type FootNote } from '@blocksuite/affine-model'; +import { + DocDisplayMetaProvider, + LinkPreviewerService, + ThemeProvider, +} from '@blocksuite/affine-shared/services'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import type { BlockStdScope } from '@blocksuite/block-std'; -import { WithDisposable } from '@blocksuite/global/utils'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; import { DualLinkIcon, LinkIcon } from '@blocksuite/icons/lit'; +import { computed, signal } from '@preact/signals-core'; import { css, html, LitElement, type TemplateResult } from 'lit'; import { property } from 'lit/decorators.js'; -import { getAttachmentFileIcon } from '../../../../../icons'; +import { + DarkLoadingIcon, + getAttachmentFileIcon, + LightLoadingIcon, + WebIcon16, +} from '../../../../../icons'; import { RefNodeSlotsProvider } from '../../../../extension/ref-node-slots'; -export class FootNotePopup extends WithDisposable(LitElement) { +export class FootNotePopup extends SignalWatcher(WithDisposable(LitElement)) { static override styles = css` .footnote-popup-container { border-radius: 4px; @@ -21,7 +31,13 @@ export class FootNotePopup extends WithDisposable(LitElement) { } `; - private readonly _prefixIcon = () => { + private readonly _isLoading$ = signal(false); + + private readonly _linkPreview$ = signal< + { favicon: string | undefined; title?: string } | undefined + >({ favicon: undefined, title: undefined }); + + private readonly _prefixIcon$ = computed(() => { const referenceType = this.footnote.reference.type; if (referenceType === 'doc') { const docId = this.footnote.reference.docId; @@ -35,9 +51,31 @@ export class FootNotePopup extends WithDisposable(LitElement) { return undefined; } return getAttachmentFileIcon(fileType); + } else if (referenceType === 'url') { + if (this._isLoading$.value) { + return this._LoadingIcon(); + } + + const favicon = this._linkPreview$.value?.favicon; + if (!favicon) { + return undefined; + } + + const titleIconType = + favicon.split('.').pop() === 'svg' + ? 'svg+xml' + : favicon.split('.').pop(); + const titleIcon = html` + ${WebIcon16} + `; + return titleIcon; } return undefined; - }; + }); private readonly _suffixIcon = (): TemplateResult | undefined => { const referenceType = this.footnote.reference.type; @@ -49,7 +87,7 @@ export class FootNotePopup extends WithDisposable(LitElement) { return undefined; }; - private readonly _popupLabel = () => { + private readonly _popupLabel$ = computed(() => { const referenceType = this.footnote.reference.type; let label = ''; const { docId, fileName, url } = this.footnote.reference; @@ -70,11 +108,23 @@ export class FootNotePopup extends WithDisposable(LitElement) { if (!url) { return label; } - // TODO(@chen): get url title from url, need to implement after LinkPreviewer refactored as an extension - label = url; + label = this._linkPreview$.value?.title ?? url; break; } return label; + }); + + private readonly _tooltip$ = computed(() => { + const referenceType = this.footnote.reference.type; + if (referenceType === 'url') { + return this.footnote.reference.url ?? ''; + } + return this._popupLabel$.value; + }); + + private readonly _LoadingIcon = () => { + const theme = this.std.get(ThemeProvider).theme; + return theme === ColorScheme.Light ? LightLoadingIcon : DarkLoadingIcon; }; /** @@ -103,14 +153,35 @@ export class FootNotePopup extends WithDisposable(LitElement) { this.abortController.abort(); }; + override connectedCallback() { + super.connectedCallback(); + if (this.footnote.reference.type === 'url' && this.footnote.reference.url) { + this._isLoading$.value = true; + this.std.store + .get(LinkPreviewerService) + .query(this.footnote.reference.url) + .then(data => { + this._linkPreview$.value = { + favicon: data.icon ?? undefined, + title: data.title ?? undefined, + }; + }) + .catch(console.error) + .finally(() => { + this._isLoading$.value = false; + }); + } + } + override render() { return html`
`;