fix(app): request attachment images at their real mosaic tile size (#1888)

This commit is contained in:
Hampus
2026-08-24 12:39:56 +02:00
committed by GitHub
parent 3a064dd728
commit 188f783fae
8 changed files with 216 additions and 128 deletions
@@ -38,3 +38,53 @@ export function splitMediaAndFileAttachments(attachments: ReadonlyArray<MessageA
}
return {mediaAttachments, fileAttachments};
}
const MOSAIC_TILE_GAP = 4;
interface MosaicTileSlot {
columns: number;
span: number;
aspectWidth: number;
aspectHeight: number;
}
const MOSAIC_TILE_SLOTS: Record<number, (index: number) => MosaicTileSlot> = {
2: () => ({columns: 2, span: 1, aspectWidth: 1, aspectHeight: 1}),
3: (index) => ({columns: 3, span: index === 0 ? 2 : 1, aspectWidth: 1, aspectHeight: 1}),
4: () => ({columns: 2, span: 1, aspectWidth: 3, aspectHeight: 2}),
5: (index) =>
index < 2
? {columns: 6, span: 3, aspectWidth: 3, aspectHeight: 2}
: {columns: 6, span: 2, aspectWidth: 1, aspectHeight: 1},
6: () => ({columns: 3, span: 1, aspectWidth: 1, aspectHeight: 1}),
7: (index) =>
index === 0
? {columns: 1, span: 1, aspectWidth: 16, aspectHeight: 9}
: {columns: 3, span: 1, aspectWidth: 1, aspectHeight: 1},
8: (index) =>
index < 2
? {columns: 2, span: 1, aspectWidth: 3, aspectHeight: 2}
: {columns: 3, span: 1, aspectWidth: 1, aspectHeight: 1},
9: () => ({columns: 3, span: 1, aspectWidth: 1, aspectHeight: 1}),
10: (index) =>
index === 0
? {columns: 1, span: 1, aspectWidth: 16, aspectHeight: 9}
: {columns: 3, span: 1, aspectWidth: 1, aspectHeight: 1},
};
const FALLBACK_MOSAIC_TILE_SLOT: MosaicTileSlot = {columns: 2, span: 1, aspectWidth: 3, aspectHeight: 2};
export function getMosaicTileBox(
count: number,
index: number,
mosaicWidth: number,
): {width: number; height: number; aspectRatio: string} {
const slot = MOSAIC_TILE_SLOTS[count]?.(index) ?? FALLBACK_MOSAIC_TILE_SLOT;
const track = (mosaicWidth - (slot.columns - 1) * MOSAIC_TILE_GAP) / slot.columns;
const width = Math.max(1, track * slot.span + (slot.span - 1) * MOSAIC_TILE_GAP);
return {
width,
height: Math.max(1, (width * slot.aspectHeight) / slot.aspectWidth),
aspectRatio: `${slot.aspectWidth} / ${slot.aspectHeight}`,
};
}
@@ -18,7 +18,11 @@ import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {UploadingAttachment} from '@app/features/messaging/models/UploadingAttachment';
import {getEffectiveAttachmentExpiry} from '@app/features/messaging/utils/AttachmentExpiryUtils';
import {getAttachmentMediaDimensions} from '@app/features/messaging/utils/MediaDimensionConfig';
import {
getAttachmentMediaDimensions,
isUsefulVisualMediaSize,
} from '@app/features/messaging/utils/MediaDimensionConfig';
import {resolveProxyRequestSize} from '@app/features/messaging/utils/MediaProxyRequestSize';
import {
buildAnimatedImageProxyURL,
buildMediaProxyURL,
@@ -256,20 +260,21 @@ const AttachmentMedia: FC<AttachmentMediaProps & {message?: Message; isPreview?:
maxHeight: attachmentDimensions.maxHeight,
responsive: true,
});
const {dimensions} = mediaCalculator.calculate(
{
width: attachment.width!,
height: attachment.height!,
},
{forceScale: true},
const {dimensions} = mediaCalculator.calculate({
width: attachment.width!,
height: attachment.height!,
});
const requestedSize = resolveProxyRequestSize(
dimensions.width,
dimensions.height,
attachment.width!,
attachment.height!,
);
const targetWidth = Math.round(dimensions.width * 2);
const targetHeight = Math.round(dimensions.height * 2);
const proxySrc = attachment.proxy_url ?? attachment.url ?? '';
const optimizedSrc = buildMediaProxyURL(proxySrc, {
format: resolvePreferredImageFormat(attachment.content_type),
width: targetWidth,
height: targetHeight,
width: requestedSize?.width,
height: requestedSize?.height,
animated: attachmentIsAnimated,
});
return (
@@ -291,7 +296,7 @@ const AttachmentMedia: FC<AttachmentMediaProps & {message?: Message; isPreview?:
height={dimensions.height}
placeholder={attachment.placeholder}
constrain={true}
alt={attachment.title || attachment.description}
alt={attachment.description ?? undefined}
nsfw={nsfw}
channelId={message?.channelId}
messageId={message?.id}
@@ -368,6 +373,7 @@ export const Attachment: FC<AttachmentProps> = observer(
attachment={enrichedAttachment}
isPreview={isPreview}
message={message}
spoilerHidden={spoilerHidden}
data-flx="channel.embeds.attachments.attachment.attachment-file"
/>,
),
@@ -377,7 +383,10 @@ export const Attachment: FC<AttachmentProps> = observer(
if (renderInMosaic && isMediaAttachment(att)) {
return null;
}
if (!inlineAttachmentMedia && (isImageType(att.content_type) || isVideoType(att.content_type))) {
const isVisualType = isImageType(att.content_type) || isVideoType(att.content_type);
const tooSmallToShowInline =
isVisualType && !isUsefulVisualMediaSize(att.width ?? 0, att.height ?? 0, getAttachmentMediaDimensions());
if ((!inlineAttachmentMedia || tooSmallToShowInline) && isVisualType) {
return renderWithFootnote(
wrapSpoiler(
<FocusRing
@@ -389,6 +398,7 @@ export const Attachment: FC<AttachmentProps> = observer(
attachment={enrichedAttachment}
isPreview={isPreview}
message={message}
spoilerHidden={spoilerHidden}
data-flx="channel.embeds.attachments.attachment.attachment-file--2"
/>
</FocusRing>,
@@ -455,6 +465,7 @@ export const Attachment: FC<AttachmentProps> = observer(
attachment={enrichedAttachment}
isPreview={isPreview}
message={message}
spoilerHidden={spoilerHidden}
data-flx="channel.embeds.attachments.attachment.attachment-file--3"
/>
</FocusRing>,
@@ -548,6 +559,7 @@ export const Attachment: FC<AttachmentProps> = observer(
attachment={att}
isPreview={isPreview}
message={message}
spoilerHidden={spoilerHidden}
data-flx="channel.embeds.attachments.attachment.attachment-file--4"
/>
</FocusRing>
@@ -9,6 +9,7 @@ import {MatureMediaBlurOverlay} from '@app/features/channel/components/embeds/Ma
import {getMediaButtonVisibility} from '@app/features/channel/components/embeds/media/MediaButtonUtils';
import {MediaContainer} from '@app/features/channel/components/embeds/media/MediaContainer';
import {MediaActionBottomSheet} from '@app/features/channel/components/MediaActionBottomSheet';
import {getMosaicTileBox} from '@app/features/channel/components/MessageAttachmentUtils';
import {useMaybeMessageViewContext} from '@app/features/channel/components/MessageViewContext';
import * as FavoriteMemeCommands from '@app/features/expressions/commands/FavoriteMemeCommands';
import {AddFavoriteMemeModal} from '@app/features/expressions/components/modals/AddFavoriteMemeModal';
@@ -18,11 +19,13 @@ import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
import {useDeleteAttachment} from '@app/features/messaging/hooks/useDeleteAttachment';
import {useMatureMedia} from '@app/features/messaging/hooks/useMatureMedia';
import {useMediaLoading} from '@app/features/messaging/hooks/useMediaLoading';
import {useMediaViewerHoverWarm} from '@app/features/messaging/hooks/useMediaViewerHoverWarm';
import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport';
import {useOpenInBrowserOnMiddleClick} from '@app/features/messaging/hooks/useOpenInBrowserOnMiddleClick';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {createDownloadHandler} from '@app/features/messaging/utils/FileDownloadUtils';
import {getMosaicMediaDimensions} from '@app/features/messaging/utils/MediaDimensionConfig';
import {resolveProxyRequestSize} from '@app/features/messaging/utils/MediaProxyRequestSize';
import {
buildAnimatedImageProxyURL,
buildMediaProxyURL,
@@ -31,6 +34,7 @@ import {
} from '@app/features/messaging/utils/MediaProxyUtils';
import {
attachmentsToViewerItems,
attachmentToViewerItem,
determineMediaType,
findViewerItemIndex,
} from '@app/features/messaging/utils/MediaViewerItemUtils';
@@ -43,7 +47,7 @@ import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuComma
import * as MediaViewerCommands from '@app/features/ui/commands/MediaViewerCommands';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import MobileLayout from '@app/features/ui/state/MobileLayout';
import {snapMediaProxyImageSize} from '@app/features/user/utils/AvatarUtils';
import {MessageAttachmentFlags} from '@fluxer/constants/src/ChannelConstants';
import type {MessageAttachment} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import {msg} from '@lingui/core/macro';
@@ -109,10 +113,9 @@ export const AttachmentGridItem: FC<AttachmentGridItemProps> = observer(
const isAudio = attachmentMediaType === 'audio';
const isGifv = attachmentMediaType === 'gifv';
const isAnimatedGif = attachmentMediaType === 'gif' || isGifv;
const isMobile = MobileLayout.enabled;
const isSpoiler = (attachment.flags & MessageAttachmentFlags.IS_SPOILER) !== 0;
const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0;
const shouldAnimateGif = useShouldAnimate({kind: 'gif'});
const shouldAnimateGif = useShouldAnimate({kind: 'gif', isAnimated: isAnimatedGif});
const {hidden: spoilerHidden, reveal: revealSpoiler} = useSpoilerState(isSpoiler, message?.channelId);
const {shouldBlur, gateReason, canReveal, reveal: revealSensitiveMedia} = useMatureMedia(nsfw, message?.channelId);
const wrapSpoiler = (node: ReactElement) =>
@@ -129,18 +132,22 @@ export const AttachmentGridItem: FC<AttachmentGridItemProps> = observer(
node
);
const mosaicDimensions = getMosaicMediaDimensions();
const maxMosaicWidth = mosaicDimensions.maxWidth;
let targetWidth = maxMosaicWidth;
let targetHeight = maxMosaicWidth;
if (
const tileIndex = mediaAttachments.findIndex((candidate) => candidate.id === attachment.id);
const tileBox = getMosaicTileBox(mediaAttachments.length, Math.max(0, tileIndex), mosaicDimensions.maxWidth);
const hasIntrinsicSize =
typeof attachment.width === 'number' &&
attachment.width > 0 &&
typeof attachment.height === 'number' &&
attachment.height > 0
) {
targetWidth = Math.min(attachment.width, maxMosaicWidth * 2);
targetHeight = Math.max(1, Math.round((targetWidth / attachment.width) * attachment.height));
}
attachment.height > 0;
const snappedTileWidth = snapMediaProxyImageSize(tileBox.width, true);
const coverWidth = hasIntrinsicSize
? Math.max(tileBox.width, (tileBox.height * attachment.width!) / attachment.height!)
: tileBox.width;
const requestedSize = hasIntrinsicSize
? resolveProxyRequestSize(coverWidth, tileBox.height, attachment.width!, attachment.height!)
: {width: snappedTileWidth, height: snappedTileWidth};
const targetWidth = requestedSize?.width;
const targetHeight = requestedSize?.height;
const proxyUrl = attachment.proxy_url ?? attachment.url ?? '';
const isBlob = proxyUrl.startsWith('blob:');
const isPlainGif = attachmentMediaType === 'gif';
@@ -159,15 +166,11 @@ export const AttachmentGridItem: FC<AttachmentGridItemProps> = observer(
height: targetHeight,
animated: isAnimatedGif && shouldAnimateGif,
});
const {ref: visibilityRef, isNearViewport} = useNearViewport<HTMLDivElement>({
disabled: !isMobile,
rememberKey: thumbnailSrc,
});
const shouldLoadMedia = isNearViewport && !shouldBlur;
const {ref: visibilityRef, isNearViewport} = useNearViewport<HTMLDivElement>({rememberKey: thumbnailSrc});
const shouldLoadMedia = isNearViewport && !shouldBlur && !spoilerHidden;
const {
loaded,
error,
cachedOnMount,
thumbHashURL,
ref: mediaRef,
onLoad: handleImageLoad,
@@ -179,6 +182,11 @@ export const AttachmentGridItem: FC<AttachmentGridItemProps> = observer(
const isFavorited = attachment.content_hash
? memes.some((meme) => meme.contentHash === attachment.content_hash)
: false;
const viewerWarmItem = useMemo(() => attachmentToViewerItem(attachment), [attachment]);
const {scheduleViewerWarm, cancelViewerWarm} = useMediaViewerHoverWarm(viewerWarmItem, {
allowAnimated: shouldAnimateGif,
enabled: !shouldBlur,
});
const handleClick = useCallback(
(event: MouseEvent | KeyboardEvent) => {
if (shouldBlur) {
@@ -355,6 +363,8 @@ export const AttachmentGridItem: FC<AttachmentGridItemProps> = observer(
tabIndex={0}
className={styles.clickableButton}
onClick={handleClick}
onMouseEnter={scheduleViewerWarm}
onMouseLeave={cancelViewerWarm}
onMouseDown={openInBrowser.onMouseDown}
onAuxClick={openInBrowser.onAuxClick}
onKeyDown={handleClick}
@@ -388,6 +398,18 @@ export const AttachmentGridItem: FC<AttachmentGridItemProps> = observer(
GIF
</div>
)}
<img
src={shouldLoadMedia ? thumbnailSrc : undefined}
ref={mediaRef}
alt={attachment.filename}
loading="eager"
draggable={false}
className={clsx(styles.mediaImage, shouldBlur && styles.mediaBlurred)}
aria-hidden={shouldBlur}
onLoad={handleImageLoad}
onError={handleImageError}
data-flx="channel.embeds.attachments.attachment-grid-item.media-image"
/>
<AnimatePresence data-flx="channel.embeds.attachments.attachment-grid-item.animate-presence">
{shouldRenderPlaceholder && thumbHashURL && (
<motion.img
@@ -397,26 +419,12 @@ export const AttachmentGridItem: FC<AttachmentGridItemProps> = observer(
transition={{duration: Accessibility.useReducedMotion ? 0 : 0.3}}
src={thumbHashURL}
alt=""
aria-hidden={true}
className={styles.placeholderImage}
data-flx="channel.embeds.attachments.attachment-grid-item.placeholder-image"
/>
)}
</AnimatePresence>
<motion.img
src={shouldLoadMedia ? thumbnailSrc : undefined}
ref={mediaRef}
alt={attachment.filename}
loading={isMobile ? 'lazy' : 'eager'}
draggable={false}
className={clsx(styles.mediaImage, shouldBlur && styles.mediaBlurred)}
aria-hidden={shouldBlur}
onLoad={handleImageLoad}
onError={handleImageError}
initial={{opacity: cachedOnMount ? 1 : 0}}
animate={{opacity: shouldRenderPlaceholder ? 0 : 1}}
transition={{duration: cachedOnMount || Accessibility.useReducedMotion ? 0 : 0.3}}
data-flx="channel.embeds.attachments.attachment-grid-item.media-image"
/>
<AltTextBadge
altText={attachment.description}
onPopoutToggle={messageViewContext?.onPopoutToggle}
@@ -4,7 +4,9 @@ import {
AttachmentGridItem,
type LayoutType,
} from '@app/features/channel/components/embeds/attachments/AttachmentGridItem';
import {getMosaicTileBox} from '@app/features/channel/components/MessageAttachmentUtils';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {getMosaicMediaDimensions} from '@app/features/messaging/utils/MediaDimensionConfig';
import styles from '@app/features/theme/styles/AttachmentLayoutGrid.module.css';
import type {MessageAttachment} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import {observer} from 'mobx-react-lite';
@@ -20,55 +22,18 @@ export interface AttachmentLayoutGridProps {
interface LayoutConfig {
type: LayoutType;
gridClassName: string;
getAspectRatio: (index: number) => string | undefined;
}
const LAYOUT_CONFIGS: Record<number, LayoutConfig> = {
2: {
type: 'two',
gridClassName: styles.twoImageGrid,
getAspectRatio: () => '1 / 1',
},
3: {
type: 'three',
gridClassName: styles.threeImageGrid,
getAspectRatio: (index) => (index === 0 ? undefined : '1 / 1'),
},
4: {
type: 'four',
gridClassName: styles.fourImageGrid,
getAspectRatio: () => '3 / 2',
},
5: {
type: 'five',
gridClassName: styles.fiveImageGrid,
getAspectRatio: (index) => (index < 2 ? '3 / 2' : '1 / 1'),
},
6: {
type: 'six',
gridClassName: styles.sixImageGrid,
getAspectRatio: () => '1 / 1',
},
7: {
type: 'seven',
gridClassName: styles.sevenImageContainer,
getAspectRatio: (index) => (index === 0 ? '16 / 9' : '1 / 1'),
},
8: {
type: 'eight',
gridClassName: styles.eightImageContainer,
getAspectRatio: (index) => (index < 2 ? '3 / 2' : '1 / 1'),
},
9: {
type: 'nine',
gridClassName: styles.nineImageGrid,
getAspectRatio: () => '1 / 1',
},
10: {
type: 'ten',
gridClassName: styles.tenImageContainer,
getAspectRatio: (index) => (index === 0 ? '16 / 9' : '1 / 1'),
},
2: {type: 'two', gridClassName: styles.twoImageGrid},
3: {type: 'three', gridClassName: styles.threeImageGrid},
4: {type: 'four', gridClassName: styles.fourImageGrid},
5: {type: 'five', gridClassName: styles.fiveImageGrid},
6: {type: 'six', gridClassName: styles.sixImageGrid},
7: {type: 'seven', gridClassName: styles.sevenImageContainer},
8: {type: 'eight', gridClassName: styles.eightImageContainer},
9: {type: 'nine', gridClassName: styles.nineImageGrid},
10: {type: 'ten', gridClassName: styles.tenImageContainer},
};
function getLayoutConfig(count: number): LayoutConfig {
@@ -79,6 +44,8 @@ export const AttachmentLayoutGrid: FC<AttachmentLayoutGridProps> = observer(
({attachments, message, isPreview, snapshotIndex}) => {
const count = attachments.length;
const config = getLayoutConfig(count);
const mosaicWidth = getMosaicMediaDimensions().maxWidth;
const tileAspectRatio = (index: number) => getMosaicTileBox(count, index, mosaicWidth).aspectRatio;
if (count === 7) {
return (
<div
@@ -89,7 +56,7 @@ export const AttachmentLayoutGrid: FC<AttachmentLayoutGridProps> = observer(
<AttachmentGridItem
key={attachments[0].id}
attachment={attachments[0]}
targetAspectRatio={config.getAspectRatio(0)}
targetAspectRatio={tileAspectRatio(0)}
message={message}
mediaAttachments={attachments}
isPreview={isPreview}
@@ -102,7 +69,7 @@ export const AttachmentLayoutGrid: FC<AttachmentLayoutGridProps> = observer(
<AttachmentGridItem
key={attachment.id}
attachment={attachment}
targetAspectRatio={config.getAspectRatio(index + 1)}
targetAspectRatio={tileAspectRatio(index + 1)}
message={message}
mediaAttachments={attachments}
isPreview={isPreview}
@@ -128,7 +95,7 @@ export const AttachmentLayoutGrid: FC<AttachmentLayoutGridProps> = observer(
<AttachmentGridItem
key={attachment.id}
attachment={attachment}
targetAspectRatio={config.getAspectRatio(index)}
targetAspectRatio={tileAspectRatio(index)}
message={message}
mediaAttachments={attachments}
isPreview={isPreview}
@@ -145,7 +112,7 @@ export const AttachmentLayoutGrid: FC<AttachmentLayoutGridProps> = observer(
<AttachmentGridItem
key={attachment.id}
attachment={attachment}
targetAspectRatio={config.getAspectRatio(index + 2)}
targetAspectRatio={tileAspectRatio(index + 2)}
message={message}
mediaAttachments={attachments}
isPreview={isPreview}
@@ -167,7 +134,7 @@ export const AttachmentLayoutGrid: FC<AttachmentLayoutGridProps> = observer(
<AttachmentGridItem
key={attachments[0].id}
attachment={attachments[0]}
targetAspectRatio={config.getAspectRatio(0)}
targetAspectRatio={tileAspectRatio(0)}
message={message}
mediaAttachments={attachments}
isPreview={isPreview}
@@ -180,7 +147,7 @@ export const AttachmentLayoutGrid: FC<AttachmentLayoutGridProps> = observer(
<AttachmentGridItem
key={attachment.id}
attachment={attachment}
targetAspectRatio={config.getAspectRatio(index + 1)}
targetAspectRatio={tileAspectRatio(index + 1)}
message={message}
mediaAttachments={attachments}
isPreview={isPreview}
@@ -198,7 +165,7 @@ export const AttachmentLayoutGrid: FC<AttachmentLayoutGridProps> = observer(
<AttachmentGridItem
key={attachment.id}
attachment={attachment}
targetAspectRatio={config.getAspectRatio(index)}
targetAspectRatio={tileAspectRatio(index)}
message={message}
mediaAttachments={attachments}
isPreview={isPreview}
@@ -8,6 +8,7 @@ import EmbedVideo from '@app/features/channel/components/embeds/media/EmbedVideo
import {getInlineVideoLayoutConstraints} from '@app/features/channel/components/embeds/media/VideoDimensionUtils';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {getAttachmentMediaDimensions} from '@app/features/messaging/utils/MediaDimensionConfig';
import {resolveProxyRequestSize} from '@app/features/messaging/utils/MediaProxyRequestSize';
import {
buildAnimatedImageProxyURL,
buildMediaProxyURL,
@@ -72,13 +73,10 @@ export const AttachmentSingleMedia: FC<AttachmentSingleMediaProps> = observer(
'--attachment-media-max-height': remFromPx(attachmentDimensions.maxHeight),
'--attachment-media-max-width': remFromPx(attachmentDimensions.maxWidth),
};
const {dimensions} = standaloneMediaCalculator.calculate(
{
width: naturalWidth,
height: naturalHeight,
},
{forceScale: true},
);
const {dimensions} = standaloneMediaCalculator.calculate({
width: naturalWidth,
height: naturalHeight,
});
const safeProxy = attachment.proxy_url ?? attachment.url ?? '';
const safeUrl = attachment.url ?? '';
const commonProps = {
@@ -171,12 +169,11 @@ export const AttachmentSingleMedia: FC<AttachmentSingleMediaProps> = observer(
</div>,
);
}
const targetWidth = Math.round(dimensions.width * 2);
const targetHeight = Math.round(dimensions.height * 2);
const requestedSize = resolveProxyRequestSize(dimensions.width, dimensions.height, naturalWidth, naturalHeight);
const optimizedSrc = buildMediaProxyURL(attachment.proxy_url ?? attachment.url ?? '', {
format: resolvePreferredImageFormat(attachment.content_type),
width: targetWidth,
height: targetHeight,
width: requestedSize?.width,
height: requestedSize?.height,
});
return wrapSpoiler(
<div
@@ -78,10 +78,6 @@
opacity: 1;
}
.mediaImageHidden {
opacity: 0;
}
.mediaBlurred {
filter: blur(0.75rem);
opacity: 0.15;
@@ -1,10 +1,52 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {describe, expect, it} from 'vitest';
import {createCalculator} from './DimensionUtils';
import {createCalculator, fitMediaWithinBounds, mediaAspectRatioValue} from './DimensionUtils';
const ATTACHMENT_CAPS = {maxWidth: 550, maxHeight: 350};
describe('fitMediaWithinBounds', () => {
it('fits the maintainer 797x118 case to 550x81', () => {
expect(fitMediaWithinBounds({width: 797, height: 118, ...ATTACHMENT_CAPS})).toEqual({width: 550, height: 81});
});
it('binds on height in the second pass for a tall image', () => {
expect(fitMediaWithinBounds({width: 400, height: 1200, ...ATTACHMENT_CAPS})).toEqual({width: 117, height: 350});
expect(fitMediaWithinBounds({width: 1000, height: 3000, ...ATTACHMENT_CAPS})).toEqual({width: 117, height: 350});
});
it('never upscales media that is already under both caps', () => {
expect(fitMediaWithinBounds({width: 200, height: 100, ...ATTACHMENT_CAPS})).toEqual({width: 200, height: 100});
expect(fitMediaWithinBounds({width: 43, height: 48, maxWidth: 400, maxHeight: 300})).toEqual({
width: 43,
height: 48,
});
});
it('rounds twice, once per pass, rather than applying a single combined scale', () => {
const combined = (width: number, height: number, maxWidth: number, maxHeight: number) => {
const scale = Math.min(1, maxWidth / width, maxHeight / height);
return {width: Math.round(width * scale), height: Math.round(height * scale)};
};
for (const [width, height, twoPassWidth, combinedWidth] of [
[551, 351, 550, 549],
[800, 600, 466, 467],
[1200, 900, 466, 467],
] as const) {
expect(fitMediaWithinBounds({width, height, ...ATTACHMENT_CAPS})).toEqual({
width: twoPassWidth,
height: 350,
});
expect(combined(width, height, 550, 350)).toEqual({width: combinedWidth, height: 350});
expect(twoPassWidth).not.toBe(combinedWidth);
}
expect(fitMediaWithinBounds({width: 797, height: 600, ...ATTACHMENT_CAPS})).toEqual({width: 465, height: 350});
expect(fitMediaWithinBounds({width: 550, height: 350, ...ATTACHMENT_CAPS})).toEqual({width: 550, height: 350});
});
});
describe('MediaDimensionCalculator', () => {
it('does not upscale small portrait media', () => {
it('caps the wrapper at the fitted width so small media is not stretched', () => {
const calculator = createCalculator({maxWidth: 400, maxHeight: 300});
const {dimensions, style} = calculator.calculate({width: 43, height: 48});
@@ -13,20 +55,38 @@ describe('MediaDimensionCalculator', () => {
expect(style).toMatchObject({
maxWidth: 'min(100%, 2.6875rem)',
width: '100%',
aspectRatio: '43/48',
display: 'block',
aspectRatio: 43 / 48,
});
});
it('still scales large portrait media down to the max height', () => {
const calculator = createCalculator({maxWidth: 400, maxHeight: 300});
it('carries the aspect ratio as a unitless number for the maintainer case', () => {
const calculator = createCalculator(ATTACHMENT_CAPS);
const {dimensions, style} = calculator.calculate({width: 860, height: 960});
const {dimensions, style} = calculator.calculate({width: 797, height: 118});
expect(dimensions).toEqual({width: 269, height: 300});
expect(style).toMatchObject({
maxWidth: 'min(100%, 16.8125rem)',
width: '100%',
aspectRatio: '269/300',
});
expect(dimensions).toEqual({width: 550, height: 81});
expect(style.aspectRatio).toBe(550 / 81);
expect(style.aspectRatio).toBeCloseTo(6.79012, 5);
expect(typeof style.aspectRatio).toBe('number');
});
it('scales tall media down to the height cap without an off-by-one', () => {
const calculator = createCalculator(ATTACHMENT_CAPS);
const {dimensions, style} = calculator.calculate({width: 400, height: 1200});
expect(dimensions).toEqual({width: 117, height: 350});
expect(style.maxHeight).toBe('21.875rem');
expect(mediaAspectRatioValue(dimensions)).toBeCloseTo(0.334286, 6);
});
it('preserves an already fitted pair and still emits the numeric aspect ratio', () => {
const calculator = createCalculator(ATTACHMENT_CAPS);
const {dimensions, style} = calculator.calculate({width: 550, height: 81}, {preserve: true});
expect(dimensions).toEqual({width: 550, height: 81});
expect(style).toMatchObject({width: '34.375rem', maxWidth: '100%', aspectRatio: 550 / 81});
});
});
@@ -12,7 +12,6 @@ interface DimensionOptions {
maxWidth?: number;
maxHeight?: number;
preserve?: boolean;
forceScale?: boolean;
aspectRatio?: boolean;
responsive?: boolean;
}
@@ -30,7 +29,6 @@ const DEFAULT_OPTIONS: Required<DimensionOptions> = {
maxWidth: MEDIA_MAX_WIDTH,
maxHeight: MEDIA_MAX_HEIGHT,
preserve: false,
forceScale: false,
aspectRatio: true,
responsive: true,
};