1- import axios from 'axios' ;
2- import { forwardRef , memo , useCallback , useEffect , useRef , useState } from 'react' ;
1+ import { forwardRef , memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
32import { Element } from 'slate' ;
43import { useReadOnly , useSlateStatic } from 'slate-react' ;
54
65import { YjsEditor } from '@/application/slate-yjs' ;
76import { BlockType , LinkPreviewType } from '@/application/types' ;
87import { ReactComponent as LinkIcon } from '@/assets/icons/link.svg' ;
9- import emptyImageSrc from '@/assets/images/empty.png' ;
108import { usePopoverContext } from '@/components/editor/components/block-popover/BlockPopoverContext' ;
119import { EditorElementProps , LinkPreviewNode } from '@/components/editor/editor.type' ;
10+ import { buildFallbackLinkPreviewData , fetchLinkPreviewData , LinkPreviewData } from '@/utils/link-preview' ;
1211import { openUrl } from '@/utils/url' ;
1312
13+ interface RemoteLinkPreviewData {
14+ data : LinkPreviewData ;
15+ url : string ;
16+ }
17+
1418export const LinkPreview = memo (
1519 forwardRef < HTMLDivElement , EditorElementProps < LinkPreviewNode > > ( ( { node, children, ...attributes } , ref ) => {
16- const [ data , setData ] = useState < {
17- image ?: { url : string } ;
18- title : string ;
19- description : string ;
20- } | null > ( null ) ;
21- const [ notFound , setNotFound ] = useState < boolean > ( false ) ;
20+ const [ remotePreview , setRemotePreview ] = useState < RemoteLinkPreviewData | null > ( null ) ;
2221 const url = node . data . url ;
22+ const fallbackData = useMemo ( ( ) => ( url ? buildFallbackLinkPreviewData ( url ) : null ) , [ url ] ) ;
23+ const remoteData = remotePreview && remotePreview . url === url ? remotePreview . data : null ;
24+ const data = remoteData ?? fallbackData ;
2325 const previewType = node . data . preview_type ?? LinkPreviewType . Bookmark ;
2426 const isEmbed = previewType === LinkPreviewType . Embed ;
2527 const editor = useSlateStatic ( ) as YjsEditor ;
@@ -28,28 +30,31 @@ export const LinkPreview = memo(
2830 const { openPopover } = usePopoverContext ( ) ;
2931
3032 useEffect ( ( ) => {
31- if ( ! url ) return ;
33+ if ( ! url ) {
34+ setRemotePreview ( null ) ;
35+ return ;
36+ }
37+
38+ const controller = new AbortController ( ) ;
3239
33- setData ( null ) ;
40+ setRemotePreview ( null ) ;
3441 void ( async ( ) => {
3542 try {
36- setNotFound ( false ) ;
37- const response = await axios . get ( `https://api.microlink.io/?url=${ url } ` ) ;
43+ const data = await fetchLinkPreviewData ( url , controller . signal ) ;
3844
39- if ( response . data . statusCode !== 200 ) {
40- setNotFound ( true ) ;
41- return ;
45+ if ( ! controller . signal . aborted ) {
46+ setRemotePreview ( { url, data } ) ;
4247 }
43-
44- const data = response . data . data ;
45-
46- setData ( data ) ;
4748 } catch ( _ ) {
48- setNotFound ( true ) ;
49+ if ( ! controller . signal . aborted ) {
50+ setRemotePreview ( null ) ;
51+ }
4952 }
5053 } ) ( ) ;
54+
55+ return ( ) => controller . abort ( ) ;
5156 } , [ url ] ) ;
52- const imageUrl = data ?. image ?. url ;
57+ const imageUrl = data ?. image ?. url || data ?. logo ?. url ;
5358 const handleClick = useCallback ( ( ) => {
5459 if ( ! url ) {
5560 if ( ! readOnly && emptyRef . current ) {
@@ -81,28 +86,6 @@ export const LinkPreview = memo(
8186 < LinkIcon className = { 'h-6 w-6 flex-none' } />
8287 < div className = { 'truncate' } > { isEmbed ? 'Paste a link to embed' : 'Paste a link to create a bookmark' } </ div >
8388 </ div >
84- ) : notFound ? (
85- < div className = { `link-preview-not-found flex w-full min-w-0 ${ isEmbed ? 'flex-col' : 'items-center' } ` } >
86- { ! isEmbed && (
87- < div
88- className = {
89- 'link-preview-empty-thumb mr-2 flex h-[80px] w-[120px] min-w-[80px] items-center justify-center rounded border text-text-primary'
90- }
91- >
92- < img
93- src = { emptyImageSrc }
94- alt = { 'Empty state' }
95- className = { 'link-preview-empty-image h-full object-cover object-center' }
96- />
97- </ div >
98- ) }
99- < div className = { `link-preview-content flex min-w-0 flex-1 flex-col ${ isEmbed ? 'p-4' : '' } ` } >
100- < div className = { 'link-preview-title text-function-error' } >
101- The link cannot be previewed. Click to open in a new tab.
102- </ div >
103- < div className = { 'link-preview-url text-sm text-text-secondary' } > { url } </ div >
104- </ div >
105- </ div >
10689 ) : (
10790 < >
10891 { imageUrl && (
@@ -128,11 +111,15 @@ export const LinkPreview = memo(
128111 >
129112 { data ?. title }
130113 </ div >
131- < div
132- className = { 'link-preview-description max-h-[64px] overflow-hidden truncate text-sm text-text-primary' }
133- >
134- { data ?. description }
135- </ div >
114+ { data ?. description && (
115+ < div
116+ className = {
117+ 'link-preview-description max-h-[64px] overflow-hidden truncate text-sm text-text-primary'
118+ }
119+ >
120+ { data . description }
121+ </ div >
122+ ) }
136123 < div className = { 'link-preview-url truncate whitespace-nowrap text-xs text-text-secondary' } > { url } </ div >
137124 </ div >
138125 </ >
0 commit comments