CI / Build & Quality Checks (pull_request) Failing after 3m21s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
Giphy and Tenor link previews forced every GIF into the same box — full card width, max 200px tall, object-fit: cover — whatever its shape: a square GIF lost half its height, a portrait one three quarters (only the middle band showed, so captions and faces at the top/bottom were cut), and a 100×80 GIF was blown up 4×. utils/gifPreviewSize.ts (unit-tested) fits the GIF inside 400×320 from og:image:width/height, keeping its aspect ratio and enlarging small GIFs at most 2×. The card centres the GIF on the surface colour with object-fit: contain, reserves the box before it loads, and the GIF badge sits on the GIF's corner. Without dimensions it shows at natural size, capped by the card. The >10 MB thumbnail fallback is requested at 2× the box (800×640) so it isn't upscaled. Verified headless (fixture previews, desktop / Pixel 7 / 320px phone): 480×200 → 398×166, 300×300 → 320×320 (292×292 on 320px), 240×480 → 160×320, 100×80 → 200×160, all 100% visible; the no-dimension fallback and narrow cards keep the ratio. tsc clean, 1325 unit tests pass, eslint 0 errors (36 warnings, unchanged), prettier clean. Fixes #341 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
/**
|
||
* Display size for a Giphy / Tenor link-preview GIF. The card used to force
|
||
* every GIF into a full-width × 200px band with `object-fit: cover`, so square
|
||
* and portrait GIFs lost most of their height and small ones were blown up.
|
||
* Fit the GIF inside `maxWidth × maxHeight` keeping its aspect ratio, and
|
||
* enlarge a small GIF at most `maxUpscale`× so it stays sharp.
|
||
*/
|
||
|
||
/** The GIF card's width (UrlPreview is 25rem); a phone shrinks it further via CSS. */
|
||
export const GIF_PREVIEW_MAX_WIDTH = 400;
|
||
export const GIF_PREVIEW_MAX_HEIGHT = 320;
|
||
export const GIF_PREVIEW_MAX_UPSCALE = 2;
|
||
|
||
export type GifPreviewBox = { width: number; height: number };
|
||
|
||
// og:image:width / og:image:height are numbers from Synapse, but some sites
|
||
// publish them as strings; anything else (missing, 0, NaN) means "unknown".
|
||
const dimension = (value: unknown): number | undefined => {
|
||
const n = typeof value === 'string' ? Number(value) : value;
|
||
return typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : undefined;
|
||
};
|
||
|
||
/** The box to render the GIF in, or `undefined` when its size isn't known. */
|
||
export const gifPreviewBox = (
|
||
rawWidth: unknown,
|
||
rawHeight: unknown,
|
||
maxWidth = GIF_PREVIEW_MAX_WIDTH,
|
||
maxHeight = GIF_PREVIEW_MAX_HEIGHT,
|
||
maxUpscale = GIF_PREVIEW_MAX_UPSCALE,
|
||
): GifPreviewBox | undefined => {
|
||
const width = dimension(rawWidth);
|
||
const height = dimension(rawHeight);
|
||
if (!width || !height) return undefined;
|
||
const scale = Math.min(maxWidth / width, maxHeight / height, maxUpscale);
|
||
return {
|
||
width: Math.max(1, Math.round(width * scale)),
|
||
height: Math.max(1, Math.round(height * scale)),
|
||
};
|
||
};
|