diff --git a/apps/web/components/document-cards/tweet-preview.tsx b/apps/web/components/document-cards/tweet-preview.tsx index 807ba5b9a..77e90971c 100644 --- a/apps/web/components/document-cards/tweet-preview.tsx +++ b/apps/web/components/document-cards/tweet-preview.tsx @@ -1,6 +1,6 @@ "use client" -import { Suspense } from "react" +import { Suspense, useMemo } from "react" import type { Tweet } from "react-tweet/api" import { TweetBody, enrichTweet, TweetSkeleton } from "react-tweet" import { cn } from "@lib/utils" @@ -117,14 +117,52 @@ function CustomTweetMedia({ ) } +function isTweetLike(value: unknown): value is Tweet { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + "user" in value + ) +} + +function parseTweetData(data: Tweet | string): Tweet | null { + if (!data) return null + if (typeof data !== "string") return isTweetLike(data) ? data : null + try { + const parsed: unknown = JSON.parse(data) + if (isTweetLike(parsed)) return parsed + console.warn("TweetPreview: parsed value did not match Tweet shape") + return null + } catch (error) { + console.warn("TweetPreview: failed to parse tweet data", error) + return null + } +} + +function TweetPreviewFallback({ noBgColor }: { noBgColor?: boolean }) { + return ( +
+ Tweet preview unavailable +
+ ) +} + export function TweetPreview({ data, noBgColor, }: { - data: Tweet + data: Tweet | string noBgColor?: boolean }) { - const parsedTweet = typeof data === "string" ? JSON.parse(data) : data + const parsedTweet = useMemo(() => parseTweetData(data), [data]) + if (!parsedTweet) return const tweet = enrichTweet(parsedTweet) return (