Giphy/Tenor link previews are a fixed ~400×200 box for every GIF: tall and square GIFs are cropped to their middle, small ones upscaled #341

Closed
opened 2026-10-10 22:15:43 -04:00 by jared · 0 comments
Owner

The GIF link-preview card (GifCard in src/app/components/url-preview/UrlPreviewCard.tsx:2263, styles GifThumbnailWrapper / GifThumbnailImg in UrlPreview.css.tsx:958-985, added in 657ca3a5 "feat: GIF previews") always renders the GIF at 100% of the card width, max 200 px tall, object-fit: cover. The GIF's own size is never used, although Synapse's preview response includes og:image:width / og:image:height (Giphy and Tenor both publish them). Every giphy.com and tenor.com link gets the same box, whatever its shape. Reported by a user: "giphy embeds are a static resolution and do not adjust depending on the gif."

Measured (local Synapse; preview_url served from fixtures shaped like Synapse's Giphy response, GIFs with a red top bar and green bottom bar):

GIF desktop box phone box (Pixel 7) shown
wide 480×200 398×166 377×157 100%
square 300×300 398×200 377×200 50% of the height (scaled 1.33×)
tall 240×480 398×200 377×200 25% of the height (scaled 1.66×)
tiny 100×80 398×200 377×200 63% of the height, upscaled 4×

In the tall case the top and bottom bars are cut off and only the middle quarter shows, blurry from upscaling (screenshot attached). Reaction GIFs are very often square or portrait, so this hits most of them: faces and captions at the top or bottom are what gets cut off.

Fix:

  • Size the box from og:image:width / og:image:height, falling back to the <img>'s naturalWidth/Height on load:
    • width = min(natural width, card width);
    • height from the aspect ratio, capped (e.g. 320–360 px);
    • for anything taller, narrow the box and keep the ratio, or use object-fit: contain on the surface colour. Don't crop.
  • Don't upscale beyond the natural size (or cap it at ~2×), so small GIFs stay sharp.
  • Reserve the box from the og dimensions before the image loads, to avoid a layout jump when it arrives.
  • The same GifCard serves Tenor links (case 'tenor'), so this fixes both.

Related, upstream: a GIF sent from the GIF picker is an ordinary m.image (it carries the correct info.w/h, from the downsized rendition), so it uses upstream Cinny's image box: 400 px wide, max 600 px tall, cover. A 240×480 GIF there renders 400×600: upscaled 1.67× and 25% of the height cropped. That code is upstream (scaleYDimension, MsgTypeRenderers.tsx:518), so it's separate, but the same "don't crop, don't upscale" rule would help there too.

Before: tall 240×480 GIF cropped to its middle quarter

giphy-tall-compare.png


Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny 30e2dbcd, cinny-desktop 8f67ec7, element-call 3b0050e8.

The GIF link-preview card (`GifCard` in src/app/components/url-preview/UrlPreviewCard.tsx:2263, styles `GifThumbnailWrapper` / `GifThumbnailImg` in UrlPreview.css.tsx:958-985, added in 657ca3a5 "feat: GIF previews") always renders the GIF at **100% of the card width, max 200 px tall, `object-fit: cover`**. The GIF's own size is never used, although Synapse's preview response includes `og:image:width` / `og:image:height` (Giphy and Tenor both publish them). Every giphy.com and tenor.com link gets the same box, whatever its shape. Reported by a user: "giphy embeds are a static resolution and do not adjust depending on the gif." **Measured** (local Synapse; `preview_url` served from fixtures shaped like Synapse's Giphy response, GIFs with a red top bar and green bottom bar): | GIF | desktop box | phone box (Pixel 7) | shown | |---|---|---|---| | wide 480×200 | 398×166 | 377×157 | 100% | | square 300×300 | 398×200 | 377×200 | **50% of the height** (scaled 1.33×) | | tall 240×480 | 398×200 | 377×200 | **25% of the height** (scaled 1.66×) | | tiny 100×80 | 398×200 | 377×200 | 63% of the height, **upscaled 4×** | In the tall case the top and bottom bars are cut off and only the middle quarter shows, blurry from upscaling (screenshot attached). Reaction GIFs are very often square or portrait, so this hits most of them: faces and captions at the top or bottom are what gets cut off. **Fix:** - Size the box from `og:image:width` / `og:image:height`, falling back to the `<img>`'s `naturalWidth/Height` on load: - width = min(natural width, card width); - height from the aspect ratio, capped (e.g. 320–360 px); - for anything taller, narrow the box and keep the ratio, or use `object-fit: contain` on the surface colour. Don't crop. - Don't upscale beyond the natural size (or cap it at ~2×), so small GIFs stay sharp. - Reserve the box from the og dimensions before the image loads, to avoid a layout jump when it arrives. - The same `GifCard` serves **Tenor** links (`case 'tenor'`), so this fixes both. **Related, upstream:** a GIF sent from the GIF picker is an ordinary `m.image` (it carries the correct `info.w/h`, from the `downsized` rendition), so it uses upstream Cinny's image box: 400 px wide, max 600 px tall, `cover`. A 240×480 GIF there renders 400×600: upscaled 1.67× and 25% of the height cropped. That code is upstream (`scaleYDimension`, MsgTypeRenderers.tsx:518), so it's separate, but the same "don't crop, don't upscale" rule would help there too. **Before: tall 240×480 GIF cropped to its middle quarter** ![giphy-tall-compare.png](https://code.lotusguild.org/attachments/c6ad2362-b057-443b-a309-4ab3be274283) --- _Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny `30e2dbcd`, cinny-desktop `8f67ec7`, element-call `3b0050e8`._
jared added the priority: mediumuxarea: media labels 2026-10-10 22:15:43 -04:00
jared closed this issue 2026-10-11 00:53:26 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#341