mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): scale embed and attachment width caps with zoom (#1775)
This commit is contained in:
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%',
|
||||
}}
|
||||
|
||||
Reference in new issue
Block a user