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.
This commit is contained in:
vimzh 2026-05-24 12:44:28 +05:30
parent 0944e5fb33
commit 3fe7eb83d1

View file

@ -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 (
<div
className={cn(
"w-full min-w-0 text-center text-[13px] text-[#737373]",
noBgColor ? "bg-transparent py-4" : "bg-black rounded-[18px] p-4",
dmSansClassName(),
)}
>
Tweet preview unavailable
</div>
)
}
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 <TweetPreviewFallback noBgColor={noBgColor} />
const tweet = enrichTweet(parsedTweet)
return (