feat(editor): add link preview to footnote popup (#9869)

[BS-2399](https://linear.app/affine-design/issue/BS-2399/ai-link-的预览支持:获取-fav-icon-标题)
This commit is contained in:
donteatfriedrice
2025-01-23 09:31:20 +00:00
parent 02bcecde72
commit bdc8dd8d5f
4 changed files with 93 additions and 15 deletions

View File

@@ -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 [];
}
},

View File

@@ -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()],
},
};
},

View File

@@ -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}
</div>`
: nothing}
<div class="popup-chip-label">${this.label}</div>
<div class="popup-chip-label" title=${this.tooltip}>${this.label}</div>
${this.suffixIcon
? html`<div class="suffix-icon" @click=${this.onSuffixClick}>
${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;

View File

@@ -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`<object
type="image/${titleIconType}"
data=${favicon}
draggable="false"
>
${WebIcon16}
</object>`;
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`
<div class="footnote-popup-container">
<footnote-popup-chip
.prefixIcon=${this._prefixIcon()}
.label=${this._popupLabel()}
.prefixIcon=${this._prefixIcon$.value}
.label=${this._popupLabel$.value}
.suffixIcon=${this._suffixIcon()}
.onClick=${this._onChipClick}
.tooltip=${this._tooltip$.value}
></footnote-popup-chip>
</div>
`;