From 3fe7eb83d190d4988771f62e7b1623d5f6dbe070 Mon Sep 17 00:00:00 2001 From: vimzh Date: Sun, 24 May 2026 12:44:28 +0530 Subject: [PATCH] fix(tweet-preview): shape-check, fallback UI, memoize, and warn on bad data Copilot review flagged three things: 1. Casting an arbitrary parsed object to Tweet without validating shape can still crash inside enrichTweet. Added an isTweetLike type guard that requires the 'user' field, which both CustomTweetHeader and the downstream enrichTweet expect. 2. Returning null on bad data silently hid the failure. Now render a small 'Tweet preview unavailable' fallback (matching the style of the existing fallback in document-modal/content/tweet.tsx) and emit a console.warn so failures show up during debugging. 3. JSON.parse was running on every render. Wrapped in useMemo keyed by the data prop so reparsing only happens when the input changes. --- .../document-cards/tweet-preview.tsx | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/web/components/document-cards/tweet-preview.tsx b/apps/web/components/document-cards/tweet-preview.tsx index 984b469c..77e90971 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,19 +117,43 @@ 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 data + if (typeof data !== "string") return isTweetLike(data) ? data : null try { - const parsed = JSON.parse(data) - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Tweet) - : null - } catch { + 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, @@ -137,8 +161,8 @@ export function TweetPreview({ data: Tweet | string noBgColor?: boolean }) { - const parsedTweet = parseTweetData(data) - if (!parsedTweet) return null + const parsedTweet = useMemo(() => parseTweetData(data), [data]) + if (!parsedTweet) return const tweet = enrichTweet(parsedTweet) return (