/** * 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)), }; };