diff --git a/src/renderer/src/components/Content/index.tsx b/src/renderer/src/components/Content/index.tsx
index e6d99af0..137a858d 100644
--- a/src/renderer/src/components/Content/index.tsx
+++ b/src/renderer/src/components/Content/index.tsx
@@ -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(
+
+ )
+ }
+
// 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) {
diff --git a/src/renderer/src/components/WebPreview/index.tsx b/src/renderer/src/components/WebPreview/index.tsx
new file mode 100644
index 00000000..090ba0cc
--- /dev/null
+++ b/src/renderer/src/components/WebPreview/index.tsx
@@ -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 (
+
{
+ e.stopPropagation()
+ window.open(url, '_blank')
+ }}
+ >
+ {image &&
}
+
+
{title}
+
{description}
+
+
+ )
+}
diff --git a/src/renderer/src/hooks/useFetchWebMetadata.tsx b/src/renderer/src/hooks/useFetchWebMetadata.tsx
new file mode 100644
index 00000000..f30bbb02
--- /dev/null
+++ b/src/renderer/src/hooks/useFetchWebMetadata.tsx
@@ -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({})
+
+ useEffect(() => {
+ webService.fetchWebMetadata(url).then((metadata) => setMetadata(metadata))
+ }, [url])
+
+ return metadata
+}
diff --git a/src/renderer/src/services/web.service.ts b/src/renderer/src/services/web.service.ts
new file mode 100644
index 00000000..dfa20ece
--- /dev/null
+++ b/src/renderer/src/services/web.service.ts
@@ -0,0 +1,48 @@
+import { TWebMetadata } from '@renderer/types'
+import DataLoader from 'dataloader'
+
+class WebService {
+ static instance: WebService
+
+ private webMetadataDataLoader = new DataLoader(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
diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts
index a1a4bbc0..8a8c0f47 100644
--- a/src/renderer/src/types.ts
+++ b/src/renderer/src/types.ts
@@ -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
+}