feat: website preview

This commit is contained in:
codytseng
2024-11-28 10:54:11 +08:00
parent 292bc8f6ea
commit 3f016c63c1
5 changed files with 111 additions and 2 deletions

View File

@@ -13,6 +13,7 @@ import {
} from '../Embedded'
import ImageGallery from '../ImageGallery'
import VideoPlayer from '../VideoPlayer'
import WebPreview from '../WebPreview'
const Content = memo(
({
@@ -24,7 +25,7 @@ const Content = memo(
className?: string
size?: 'normal' | 'small'
}) => {
const { content, images, videos, embeddedNotes } = preprocess(event.content)
const { content, images, videos, embeddedNotes, lastNonMediaUrl } = preprocess(event.content)
const isNsfw = isNsfwEvent(event)
const nodes = embedded(content, [
embeddedWebsocketUrlRenderer,
@@ -62,6 +63,17 @@ const Content = memo(
})
}
// Add website preview
if (lastNonMediaUrl) {
nodes.push(
<WebPreview
className={size === 'small' ? 'mt-1' : 'mt-2'}
key={`web-preview-${event.id}`}
url={lastNonMediaUrl}
/>
)
}
// Add embedded notes
if (embeddedNotes.length) {
embeddedNotes.forEach((note, index) => {
@@ -79,6 +91,7 @@ export default Content
function preprocess(content: string) {
const urlRegex = /(https?:\/\/[^\s"']+)/g
const urls = content.match(urlRegex) || []
let lastNonMediaUrl: string | undefined
let c = content
const images: string[] = []
@@ -91,6 +104,8 @@ function preprocess(content: string) {
} else if (isVideo(url)) {
c = c.replace(url, '').trim()
videos.push(url)
} else {
lastNonMediaUrl = url
}
})
@@ -101,7 +116,7 @@ function preprocess(content: string) {
embeddedNotes.push(note)
})
return { content: c, images, videos, embeddedNotes }
return { content: c, images, videos, embeddedNotes, lastNonMediaUrl }
}
function isImage(url: string) {

View File

@@ -0,0 +1,27 @@
import { Image } from '@nextui-org/image'
import { useFetchWebMetadata } from '@renderer/hooks/useFetchWebMetadata'
import { cn } from '@renderer/lib/utils'
export default function WebPreview({ url, className }: { url: string; className?: string }) {
const { title, description, image } = useFetchWebMetadata(url)
if (!title && !description && !image) {
return null
}
return (
<div
className={cn('p-0 hover:bg-muted/50 cursor-pointer flex w-full', className)}
onClick={(e) => {
e.stopPropagation()
window.open(url, '_blank')
}}
>
{image && <Image src={image} className="rounded-l-lg object-cover w-2/5" removeWrapper />}
<div className={`flex-1 w-0 p-2 border ${image ? 'rounded-r-lg' : 'rounded-lg'}`}>
<div className="font-semibold truncate">{title}</div>
<div className="text-sm text-muted-foreground line-clamp-2">{description}</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,13 @@
import { TWebMetadata } from '@renderer/types'
import { useEffect, useState } from 'react'
import webService from '@renderer/services/web.service'
export function useFetchWebMetadata(url: string) {
const [metadata, setMetadata] = useState<TWebMetadata>({})
useEffect(() => {
webService.fetchWebMetadata(url).then((metadata) => setMetadata(metadata))
}, [url])
return metadata
}

View File

@@ -0,0 +1,48 @@
import { TWebMetadata } from '@renderer/types'
import DataLoader from 'dataloader'
class WebService {
static instance: WebService
private webMetadataDataLoader = new DataLoader<string, TWebMetadata>(async (urls) => {
return await Promise.all(
urls.map(async (url) => {
try {
const res = await fetch(url)
const html = await res.text()
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
const title =
doc.querySelector('meta[property="og:title"]')?.getAttribute('content') ||
doc.querySelector('title')?.textContent
const description =
doc.querySelector('meta[property="og:description"]')?.getAttribute('content') ||
(doc.querySelector('meta[name="description"]') as HTMLMetaElement | null)?.content
const image = (doc.querySelector('meta[property="og:image"]') as HTMLMetaElement | null)
?.content
return { title, description, image }
} catch (e) {
console.error(e)
return {}
}
})
)
})
constructor() {
if (!WebService.instance) {
WebService.instance = this
}
return WebService.instance
}
async fetchWebMetadata(url: string) {
return await this.webMetadataDataLoader.load(url)
}
}
const instance = new WebService()
export default instance

View File

@@ -16,3 +16,9 @@ export type TRelayList = {
export type TRelayInfo = {
supported_nips?: number[]
}
export type TWebMetadata = {
title?: string | null
description?: string | null
image?: string | null
}