import React, { useState } from "react"; import { formatDistanceToNow } from "date-fns"; import { clsx } from "clsx"; import { MessageSquare, Heart, Repeat2, ExternalLink, Play, } from "lucide-react"; import RichText from "./RichText"; import { Avatar } from "../ui"; import { segmentByFacets } from "../../lib/socialPost"; import type { SocialPost, SocialPostFacet } from "../../lib/socialPost"; import { displayHandle } from "../../lib/handle"; interface SocialPostEmbedProps { post: SocialPost; postUrl: string; host: string; className?: string; onOpen: (e: React.MouseEvent, url: string) => void; } function shortTimestamp(createdAt?: string): string { if (!createdAt) return ""; try { return formatDistanceToNow(new Date(createdAt), { addSuffix: false }) .replace("less than a minute", "now") .replace("about ", "") .replace(/ hours?/, "h") .replace(/ minutes?/, "m") .replace(/ months?/, "mo") .replace(/ days?/, "d") .replace(/ years?/, "y"); } catch { return ""; } } function formatCount(count?: number): string | null { if (!count) return null; if (count >= 10000) return `${Math.round(count / 1000)}K`; if (count >= 1000) return `${(count / 1000).toFixed(1).replace(/\.0$/, "")}K`; return String(count); } const FACET_LINK_CLASS = "text-primary-600 dark:text-primary-400 hover:underline"; function PostText({ text, facets, host, onOpen, }: { text: string; facets?: SocialPostFacet[]; host: string; onOpen: (e: React.MouseEvent, url: string) => void; }) { if (!facets?.length) return ; return ( <> {segmentByFacets(text, facets).map((segment, i) => { const facet = segment.facet; if (facet?.link) { const link = facet.link; return ( onOpen(e, link)} > {segment.text} ); } if (facet?.mention) { return ( e.stopPropagation()} > {segment.text} ); } if (facet?.tag) { const tagUrl = `https://${host}/hashtag/${encodeURIComponent(facet.tag)}`; return ( onOpen(e, tagUrl)} > {segment.text} ); } return {segment.text}; })} ); } export default function SocialPostEmbed({ post, postUrl, host, className, onOpen, }: SocialPostEmbedProps) { const [videoThumbError, setVideoThumbError] = useState(false); const timestamp = shortTimestamp(post.createdAt); const counts = [ { icon: MessageSquare, value: formatCount(post.replyCount) }, { icon: Repeat2, value: formatCount(post.repostCount) }, { icon: Heart, value: formatCount(post.likeCount) }, ].filter((c) => c.value); return (
{ e.preventDefault(); onOpen(e, postUrl); }} role="button" tabIndex={0} className={clsx( "block bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 hover:border-surface-300 dark:hover:border-surface-600 transition-colors group overflow-hidden cursor-pointer p-3 font-sans", className, )} >
{post.author.displayName || displayHandle(post.author.handle, post.author.did)} @{displayHandle(post.author.handle, post.author.did)} {timestamp && ( ยท {timestamp} )}
{host}
{post.text && (

)} {post.images.length > 0 && (
{post.images.slice(0, 4).map((img, i) => ( {img.alt ))}
)} {post.video && (
{post.video.thumbnail && !videoThumbError ? ( setVideoThumbError(true)} /> ) : (
)}
)} {post.gif && (
{post.gif.isVideo ? ( ) : ( {post.gif.alt )}
)} {post.external && (
{post.external.thumb && ( )}

{post.external.title || post.external.uri}

{(() => { try { return new URL(post.external.uri).hostname.replace( /^www\./, "", ); } catch { return post.external.uri; } })()}

)} {post.quote && (
{post.quote.author.displayName || displayHandle(post.quote.author.handle, post.quote.author.did)} @{displayHandle(post.quote.author.handle, post.quote.author.did)}
{post.quote.text && (

)}
)} {counts.length > 0 && (
{counts.map(({ icon: Icon, value }, i) => ( {value} ))}
)}
); }