return {
...tweet,
entities: {
...tweet.entities,
hashtags: ensureArray(entities.hashtags),
user_mentions: ensureArray(entities.user_mentions),
urls: ensureArray(entities.urls),
symbols: ensureArray(entities.symbols),
},
}
}
function parseTweetData(data: Tweet | string): Tweet | null {
if (!data) return null
let value: unknown = data
if (typeof data === "string") {
try {
value = JSON.parse(data)
} catch (error) {
console.warn("TweetPreview: failed to parse tweet data", error)
return null
}
}
if (!isTweetLike(value)) {
console.warn("TweetPreview: parsed value did not match Tweet shape")
return null
}
return normalizeTweet(value)
}
function TweetPreviewFallback({ noBgColor }: { noBgColor?: boolean }) {
return (
Tweet preview unavailable
)
}
export function TweetPreview({
data,
noBgColor,
}: {
data: Tweet | string
noBgColor?: boolean
}) {
const tweet = useMemo(() => {
const parsed = parseTweetData(data)
if (!parsed) return null
try {
return enrichTweet(parsed)
} catch (error) {
// enrichTweet still walks quoted tweets, media entities and per-entity
// indices, so a partially-malformed payload can throw here even after
// the shape check. Degrade to the fallback instead of crashing render.
console.warn("TweetPreview: failed to enrich tweet data", error)
return null
}
}, [data])
if (!tweet) return
return (
)
}