fix: complete Gif object reconstruction for draft rehydration

Fix "Cannot read property 'url' of undefined" error when rehydrating
drafts with GIFs. The Gif object was missing required properties like
url, content_description, and media_formats.preview that are needed
by useResolveGifQuery and other components.

Also preserve alt text through serialization by storing it in URL
query params alongside dimensions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-01-16 11:30:38 +02:00
parent 28da42fd61
commit 0992c9376e
+30 -12
View File
@@ -181,7 +181,7 @@ function serializeVideo(
/** /**
* Serialize GIF to server format as external embed. * Serialize GIF to server format as external embed.
* URL format: https://media.tenor.com/{id}/{filename}.gif?hh=HEIGHT&ww=WIDTH * URL format: https://media.tenor.com/{id}/{filename}.gif?hh=HEIGHT&ww=WIDTH&alt=ALT_TEXT
*/ */
function serializeGif(gifMedia: { function serializeGif(gifMedia: {
type: 'gif' type: 'gif'
@@ -195,12 +195,16 @@ function serializeGif(gifMedia: {
return undefined return undefined
} }
// Build URL with dimensions in query params // Build URL with dimensions and alt text in query params
const url = new URL(gifFormat.url) const url = new URL(gifFormat.url)
if (gifFormat.dims) { if (gifFormat.dims) {
url.searchParams.set('ww', String(gifFormat.dims[0])) url.searchParams.set('ww', String(gifFormat.dims[0]))
url.searchParams.set('hh', String(gifFormat.dims[1])) url.searchParams.set('hh', String(gifFormat.dims[1]))
} }
// Store alt text if present
if (gifMedia.alt) {
url.searchParams.set('alt', gifMedia.alt)
}
return { return {
$type: 'app.bsky.draft.defs#draftEmbedExternal', $type: 'app.bsky.draft.defs#draftEmbedExternal',
@@ -298,11 +302,11 @@ export function draftViewToSummary(
/** /**
* Parse GIF data from a Tenor URL. * Parse GIF data from a Tenor URL.
* URL format: https://media.tenor.com/{id}/{filename}.gif?hh=HEIGHT&ww=WIDTH * URL format: https://media.tenor.com/{id}/{filename}.gif?hh=HEIGHT&ww=WIDTH&alt=ALT_TEXT
*/ */
function parseGifFromUrl( function parseGifFromUrl(
uri: string, uri: string,
): {url: string; width: number; height: number} | undefined { ): {url: string; width: number; height: number; alt: string} | undefined {
try { try {
const url = new URL(uri) const url = new URL(uri)
if (url.hostname !== TENOR_HOSTNAME) { if (url.hostname !== TENOR_HOSTNAME) {
@@ -311,12 +315,13 @@ function parseGifFromUrl(
const height = parseInt(url.searchParams.get('hh') || '', 10) const height = parseInt(url.searchParams.get('hh') || '', 10)
const width = parseInt(url.searchParams.get('ww') || '', 10) const width = parseInt(url.searchParams.get('ww') || '', 10)
const alt = url.searchParams.get('alt') || ''
if (!height || !width) { if (!height || !width) {
return undefined return undefined
} }
return {url: uri, width, height} return {url: uri, width, height, alt}
} catch { } catch {
return undefined return undefined
} }
@@ -370,20 +375,33 @@ export function draftToComposerPosts(
for (const ext of post.embedExternals) { for (const ext of post.embedExternals) {
const gifData = parseGifFromUrl(ext.uri) const gifData = parseGifFromUrl(ext.uri)
if (gifData) { if (gifData) {
// Reconstruct a minimal Gif object // Reconstruct a Gif object with all required properties
// The full Gif object will need to be re-fetched from Tenor if needed const mediaObject = {
url: gifData.url,
dims: [gifData.width, gifData.height] as [number, number],
duration: 0,
size: 0,
}
embed.media = { embed.media = {
type: 'gif', type: 'gif',
gif: { gif: {
id: '', id: '',
created: 0,
hasaudio: false,
hascaption: false,
flags: '',
tags: [],
title: '',
content_description: gifData.alt || '',
itemurl: '',
url: gifData.url, // Required for useResolveGifQuery
media_formats: { media_formats: {
gif: { gif: mediaObject,
url: gifData.url, tinygif: mediaObject,
dims: [gifData.width, gifData.height], preview: mediaObject,
},
}, },
} as Gif, } as Gif,
alt: '', alt: gifData.alt,
} }
break break
} }