import { useCallback, useState } from "react"; import { Bookmark, Heart, Link2, MessageCircle, Repeat2 } from "lucide-react"; import type { Tweet } from "../lib/fetching/types"; import { timeAgo } from "../lib/utils/time"; import { LangText } from "@sortug/prosody-ui"; interface TweetCardProps { tweet: Tweet; accent: string; allowBookmarkRemoval?: boolean; onRemoveBookmark?: (tweetId: string) => void; onReply?: (tweetId: string, text: string) => Promise; onToggleRetweet?: (tweetId: string, next: boolean) => Promise; onToggleLike?: (tweetId: string, next: boolean) => Promise; onToggleBookmark?: (tweetId: string, next: boolean) => Promise; onOpenAuthor?: (author: Tweet["author"]) => void; onOpenThread?: (tweet: Tweet) => void; isQuote?: boolean; } type ActionKind = "reply" | "retweet" | "like" | "bookmark" | null; export function TweetCard(props: TweetCardProps) { const { tweet, accent, allowBookmarkRemoval, onRemoveBookmark, onOpenAuthor, onOpenThread, isQuote, } = props; const timestamp = timeAgo(tweet.time); return (
{tweet.retweeted_by && (

onOpenAuthor?.(tweet.retweeted_by!.author)} className="muted tiny retweet-banner" > Retweeted by {tweet.retweeted_by.author.name} {timeAgo(tweet.retweeted_by.time)}

)}
{tweet.author.name}
{allowBookmarkRemoval && onRemoveBookmark && ( )}
{isQuote && tweet.replyingTo.length > 0 && (
replying to {tweet.replyingTo.map((rt) => ( @{rt.username} ))}
)}
{/*renderText(tweet.text)}*/}
{!!tweet.media.pics.length && (
{tweet.media.pics.map((pic) => ( Tweet media ))}
)} {tweet.media.video.url && (
)} {!!tweet.urls.length && (
{tweet.urls.map((link) => ( {link.displayUrl} ))}
// end body )} {tweet.quoting && }
{!isQuote && }
); } type Token = { type: "text" | "mention" | "hashtag" | "url"; value: string }; function tokenize(text: string): Token[] { const tokens: Token[] = []; const regex = /(https?:\/\/\S+|@[A-Za-z0-9_]+|#[A-Za-z0-9_]+)/g; let lastIndex = 0; for (const match of text.matchAll(regex)) { const value = match[0]; const index = match.index ?? 0; if (index > lastIndex) { tokens.push({ type: "text", value: text.slice(lastIndex, index) }); } if (value.startsWith("http")) { tokens.push({ type: "url", value }); } else if (value.startsWith("@")) { tokens.push({ type: "mention", value }); } else if (value.startsWith("#")) { tokens.push({ type: "hashtag", value }); } lastIndex = index + value.length; } if (lastIndex < text.length) { tokens.push({ type: "text", value: text.slice(lastIndex) }); } return tokens; } function renderText(text: string) { return tokenize(text).map((token, index) => { if (token.type === "text") { return token.value.split("\n").map((segment, segmentIndex, arr) => ( {segment} {segmentIndex < arr.length - 1 ?
: null}
)); } if (token.type === "url") { return ( {token.value} ); } if (token.type === "mention") { return ( {token.value} ); } return ( {token.value} ); }); } function Actions(props: TweetCardProps) { const { tweet, onReply, onToggleRetweet, onToggleLike, onToggleBookmark } = props; const tweetUrl = `https://x.com/${tweet.author.username}/status/${tweet.id}`; const [copied, setCopied] = useState(false); const [pendingAction, setPendingAction] = useState(null); const copyLink = useCallback(async () => { try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(tweetUrl); } else if (typeof document !== "undefined") { const textarea = document.createElement("textarea"); textarea.value = tweetUrl; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); } setCopied(true); setTimeout(() => setCopied(false), 1800); } catch (error) { console.warn("Failed to copy tweet link", error); } }, [tweetUrl]); const executeAction = useCallback( async (kind: ActionKind, fn?: () => Promise) => { if (!fn) return; setPendingAction(kind); try { await fn(); } catch (error) { console.error(`Failed to perform ${kind ?? "action"}`, error); } finally { setPendingAction(null); } }, [], ); const handleReply = useCallback(() => { if (!onReply || typeof window === "undefined") return; const prefill = `@${tweet.author.username} `; const text = window.prompt("Reply", prefill); if (!text || !text.trim()) return; executeAction("reply", () => onReply(tweet.id, text.trim())); }, [executeAction, onReply, tweet.author.username, tweet.id]); const handleRetweet = useCallback(() => { if (!onToggleRetweet) return; const next = !tweet.rted; executeAction("retweet", () => onToggleRetweet(tweet.id, next)); }, [executeAction, onToggleRetweet, tweet.id, tweet.rted]); const handleLike = useCallback(() => { if (!onToggleLike) return; const next = !tweet.liked; executeAction("like", () => onToggleLike(tweet.id, next)); }, [executeAction, onToggleLike, tweet.id, tweet.liked]); const handleBookmark = useCallback(() => { if (!onToggleBookmark) return; const next = !tweet.bookmarked; executeAction("bookmark", () => onToggleBookmark(tweet.id, next)); }, [executeAction, onToggleBookmark, tweet.bookmarked, tweet.id]); return (
); } function Quote({ tweet }: { tweet: Tweet }) { return (
); }