fix(app): scale embed and attachment width caps with zoom (#1775)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-20 04:33:17 +02:00
1 parent df16957c95
commit ddc8837d4f
11 files changed
+65 -25

No files matched your search

@@ -5,7 +5,7 @@ import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {clsx} from 'clsx';
import type {CSSProperties, FC, ReactNode} from 'react';
import type {FC, ReactNode} from 'react';
const REVEAL_SPOILER_DESCRIPTOR = msg({
message: 'Reveal spoiler',
@@ -23,24 +23,14 @@ interface SpoilerOverlayProps {
label?: string;
inline?: boolean;
className?: string;
style?: CSSProperties;
}
export const SpoilerOverlay: FC<SpoilerOverlayProps> = ({
hidden,
onReveal,
children,
label,
inline,
className,
style,
}) => {
export const SpoilerOverlay: FC<SpoilerOverlayProps> = ({hidden, onReveal, children, label, inline, className}) => {
const {i18n} = useLingui();
const ariaLabel = label ?? i18n._(REVEAL_SPOILER_DESCRIPTOR);
return (
<div
className={clsx(styles.container, inline && styles.inline, hidden && styles.hidden, className)}
style={style}
data-flx="app.spoiler-overlay.container"
>
<div className={styles.content} aria-hidden={hidden} data-flx="app.spoiler-overlay.content">
@@ -2,6 +2,8 @@
import type {MessageAttachment} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
export const ATTACHMENT_CARD_WIDTH = 400;
const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp', 'image/avif'];
const VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'];
@@ -48,7 +48,7 @@
margin-top: var(--message-container-gap, 0.25rem);
}
.urlEmbedSpoiler {
div.urlEmbedSpoiler {
width: 100%;
max-width: 22.5rem;
}
@@ -109,7 +109,6 @@ const SpoileredUrlEmbed = observer(function SpoileredUrlEmbed({
hidden={hidden}
onReveal={reveal}
className={styles.urlEmbedSpoiler}
style={{width: '100%', maxWidth: 360}}
data-flx="channel.message-attachments.spoilered-url-embed"
>
{children}
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {ATTACHMENT_CARD_WIDTH} from '@app/features/channel/components/MessageAttachmentUtils';
import styles from '@app/features/channel/components/MessageUploadProgress.module.css';
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
import {useMessageUpload} from '@app/features/messaging/hooks/useCloudUpload';
@@ -77,8 +78,8 @@ export const MessageUploadProgress = observer(({attachment, message}: MessageUpl
}
: {
display: 'grid',
width: '400px',
maxWidth: '400px',
width: remFromPx(ATTACHMENT_CARD_WIDTH),
maxWidth: remFromPx(ATTACHMENT_CARD_WIDTH),
};
return (
<div style={containerStyles} data-flx="channel.message-upload-progress.div">
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
BLUESKY_EMBED_MEDIA_FALLBACK_OUTER_WIDTH,
BLUESKY_EMBED_MIN_CONTENT_WIDTH,
calculateBlueskyMediaContainerWidth,
calculateBlueskyOuterMaxWidth,
EMBED_TEXT_OUTER_WIDTH,
formatResponsiveEmbedWidth,
} from '@app/features/channel/components/embeds/EmbedRenderUtils';
import {describe, expect, it} from 'vitest';
describe('formatResponsiveEmbedWidth', () => {
it('caps in rem so the width tracks the root font size at any zoom', () => {
expect(formatResponsiveEmbedWidth(EMBED_TEXT_OUTER_WIDTH)).toBe('min(100%, 32.25rem)');
expect(formatResponsiveEmbedWidth(BLUESKY_EMBED_MEDIA_FALLBACK_OUTER_WIDTH)).toBe('min(100%, 27rem)');
});
it('rounds to whole pixels before converting', () => {
expect(formatResponsiveEmbedWidth(400.4)).toBe('min(100%, 25rem)');
});
it('clamps non-positive and non-finite widths', () => {
expect(formatResponsiveEmbedWidth(0)).toBe('min(100%, 0rem)');
expect(formatResponsiveEmbedWidth(Number.NaN)).toBe('min(100%, 0rem)');
});
});
describe('calculateBlueskyOuterMaxWidth', () => {
it('falls back to the shared text width when there is no media', () => {
expect(calculateBlueskyOuterMaxWidth({hasMedia: false, chromeWidth: 29})).toBe(EMBED_TEXT_OUTER_WIDTH);
});
it('adds chrome around the media container width', () => {
expect(calculateBlueskyOuterMaxWidth({mediaWidth: 480, hasMedia: true, chromeWidth: 29})).toBe(509);
});
it('never goes below the minimum content width', () => {
expect(calculateBlueskyMediaContainerWidth(120)).toBe(BLUESKY_EMBED_MIN_CONTENT_WIDTH);
});
});
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {MessageEmbedTypes} from '@fluxer/constants/src/ChannelConstants';
import type {MessageEmbed} from '@fluxer/schema/src/domains/message/EmbedSchemas';
@@ -28,7 +29,7 @@ export function isMediaOnlyEmbed(embed: MediaOnlyEmbedFields): boolean {
export const BLUESKY_EMBED_MIN_CONTENT_WIDTH = 320;
export const BLUESKY_EMBED_MEDIA_FALLBACK_OUTER_WIDTH = 432;
export const BLUESKY_EMBED_TEXT_OUTER_WIDTH = 516;
export const EMBED_TEXT_OUTER_WIDTH = 516;
const normalizePositiveWidth = (width: number): number => {
if (!Number.isFinite(width) || width <= 0) {
return 0;
@@ -55,9 +56,9 @@ export function calculateBlueskyOuterMaxWidth({
if (mediaContainerWidth !== undefined) {
return mediaContainerWidth + normalizePositiveWidth(chromeWidth);
}
return hasMedia ? BLUESKY_EMBED_MEDIA_FALLBACK_OUTER_WIDTH : BLUESKY_EMBED_TEXT_OUTER_WIDTH;
return hasMedia ? BLUESKY_EMBED_MEDIA_FALLBACK_OUTER_WIDTH : EMBED_TEXT_OUTER_WIDTH;
}
export function formatResponsiveEmbedWidth(width: number): string {
return `min(100%, ${normalizePositiveWidth(width)}px)`;
return `min(100%, ${remFromPx(normalizePositiveWidth(width))})`;
}
@@ -3,6 +3,7 @@
import {TextualAttachmentPreview} from '@app/features/channel/components/embeds/attachments/TextualAttachmentPreview';
import {splitFilename} from '@app/features/channel/components/embeds/EmbedUtils';
import {canDeleteAttachmentUtil} from '@app/features/channel/components/MessageActionUtils';
import {ATTACHMENT_CARD_WIDTH} from '@app/features/channel/components/MessageAttachmentUtils';
import {useMaybeMessageViewContext} from '@app/features/channel/components/MessageViewContext';
import {DELETE_ATTACHMENT_DESCRIPTOR, DOWNLOAD_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {useDeleteAttachment} from '@app/features/messaging/hooks/useDeleteAttachment';
@@ -181,8 +182,8 @@ export const AttachmentFile = observer(({attachment, message, isPreview}: Attach
containerStyles = {
display: 'grid',
width: '100%',
maxWidth: '400px',
minWidth: 'min(400px, 100%)',
maxWidth: remFromPx(ATTACHMENT_CARD_WIDTH),
minWidth: `min(${remFromPx(ATTACHMENT_CARD_WIDTH)}, 100%)`,
};
}
const handleDownload = async (e: React.MouseEvent) => {
@@ -23,6 +23,10 @@ import {
EmbedProvider,
EmbedTitle,
} from '@app/features/channel/components/embeds/channel_embed/EmbedParts';
import {
EMBED_TEXT_OUTER_WIDTH,
formatResponsiveEmbedWidth,
} from '@app/features/channel/components/embeds/EmbedRenderUtils';
import markupStyles from '@app/features/theme/styles/Markup.module.css';
import {MessageEmbedTypes} from '@fluxer/constants/src/ChannelConstants';
import type {EmbedMedia} from '@fluxer/schema/src/domains/message/EmbedSchemas';
@@ -58,7 +62,7 @@ export const RichEmbed: FC<EmbedProps> = observer(
data-search-highlight-scope="message"
style={{
borderLeft: `4px solid ${getBorderColor(embed.color)}`,
maxWidth: useNarrowWidth ? `min(100%, ${EMBED_MEDIA_MAX_WIDTH}px)` : 'min(100%, 516px)',
maxWidth: formatResponsiveEmbedWidth(useNarrowWidth ? EMBED_MEDIA_MAX_WIDTH : EMBED_TEXT_OUTER_WIDTH),
}}
data-flx="channel.embeds.embed.rich-embed.embed"
>
@@ -4,6 +4,7 @@ import {deriveDefaultNameFromMessage} from '@app/features/channel/components/emb
import styles from '@app/features/channel/components/embeds/media/EmbedAudio.module.css';
import {getMediaButtonVisibility} from '@app/features/channel/components/embeds/media/MediaButtonUtils';
import type {BaseMediaProps} from '@app/features/channel/components/embeds/media/MediaTypes';
import {ATTACHMENT_CARD_WIDTH} from '@app/features/channel/components/MessageAttachmentUtils';
import {useMaybeMessageViewContext} from '@app/features/channel/components/MessageViewContext';
import {DELETE_ATTACHMENT_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {useDeleteAttachment} from '@app/features/messaging/hooks/useDeleteAttachment';
@@ -130,8 +131,8 @@ const EmbedAudio: FC<EmbedAudioProps> = observer(
: {
display: 'grid',
width: '100%',
maxWidth: 'min(100%, 400px)',
minWidth: 'min(400px, 100%)',
maxWidth: `min(100%, ${remFromPx(ATTACHMENT_CARD_WIDTH)})`,
minWidth: `min(${remFromPx(ATTACHMENT_CARD_WIDTH)}, 100%)`,
};
const {showDeleteButton, showDownloadButton} = getMediaButtonVisibility(
canFavorite,
@@ -233,7 +233,7 @@ export const EmbedYouTube: FC<EmbedYouTubeProps> = observer(({embed, width = YOU
className={styles.videoContainer}
style={{
...containerStyle,
width: `${dimensions.width}px`,
width: remFromPx(dimensions.width),
aspectRatio,
maxWidth: '100%',
}}