100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import "./LinkPreview.css";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { LRUCache } from "typescript-lru-cache";
|
|
|
|
import { MediaElement } from "@/Components/Embed/MediaElement";
|
|
import Spinner from "@/Components/Icons/Spinner";
|
|
import { LinkPreviewData, NostrServices } from "@/External/NostrServices";
|
|
|
|
import { ProxyImg } from "../ProxyImg";
|
|
import GenericPlayer from "./GenericPlayer";
|
|
|
|
async function fetchUrlPreviewInfo(url: string) {
|
|
const api = new NostrServices("https://nostr.api.v0l.io");
|
|
try {
|
|
return await api.linkPreview(url.endsWith(")") ? url.slice(0, -1) : url);
|
|
} catch (e) {
|
|
console.warn(`Failed to load link preview`, url);
|
|
}
|
|
}
|
|
|
|
const cache = new LRUCache<string, LinkPreviewData>({
|
|
maxSize: 100,
|
|
});
|
|
|
|
const LinkPreview = ({ url }: { url: string }) => {
|
|
const [preview, setPreview] = useState<LinkPreviewData | null>(cache.get(url));
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
if (preview) return;
|
|
const data = await fetchUrlPreviewInfo(url);
|
|
if (data) {
|
|
const type = data.og_tags?.find(a => a[0].toLowerCase() === "og:type");
|
|
const canPreviewType = type?.[1].startsWith("image") || type?.[1].startsWith("video") || false;
|
|
if (canPreviewType || data.image) {
|
|
setPreview(data);
|
|
cache.set(url, data);
|
|
return;
|
|
}
|
|
}
|
|
|
|
setPreview(null);
|
|
})();
|
|
}, [url]);
|
|
|
|
if (preview === null)
|
|
return (
|
|
<a href={url} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
|
{url}
|
|
</a>
|
|
);
|
|
|
|
function previewElement() {
|
|
const type = preview?.og_tags?.find(a => a[0].toLowerCase() === "og:type")?.[1];
|
|
if (type?.startsWith("video")) {
|
|
const urlTags = ["og:video:secure_url", "og:video:url", "og:video"];
|
|
const link = preview?.og_tags?.find(a => urlTags.includes(a[0].toLowerCase()))?.[1];
|
|
const videoType = preview?.og_tags?.find(a => a[0].toLowerCase() === "og:video:type")?.[1] ?? "video/mp4";
|
|
if (link && videoType.startsWith("video/")) {
|
|
return <MediaElement url={link} mime={videoType} />;
|
|
}
|
|
if (link && videoType.startsWith("text/html") && preview?.image) {
|
|
return <GenericPlayer url={link} poster={preview?.image} />;
|
|
}
|
|
}
|
|
if (type?.startsWith("image")) {
|
|
const urlTags = ["og:image:secure_url", "og:image:url", "og:image"];
|
|
const link = preview?.og_tags?.find(a => urlTags.includes(a[0].toLowerCase()))?.[1];
|
|
const videoType = preview?.og_tags?.find(a => a[0].toLowerCase() === "og:image:type")?.[1] ?? "image/png";
|
|
if (link) {
|
|
return <MediaElement url={link} mime={videoType} />;
|
|
}
|
|
}
|
|
if (preview?.image) {
|
|
return <ProxyImg src={preview?.image} className="w-full object-cover aspect-video"/>;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="link-preview-container">
|
|
{preview && (
|
|
<a href={url} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
|
{previewElement()}
|
|
<div className="link-preview-title">
|
|
<h1>{preview?.title}</h1>
|
|
{preview?.description && <small>{preview.description.slice(0, 160)}</small>}
|
|
<br />
|
|
<small className="host">{new URL(url).host}</small>
|
|
</div>
|
|
</a>
|
|
)}
|
|
{!preview && <Spinner className="items-center" />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LinkPreview;
|