fix(embed): correct link thumbnail placement and harden unfurl parsing (#1672)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-16 14:10:25 +02:00
1 parent 7a45d5844d
commit 039bd1a7df
13 files changed
+1124 -196

No files matched your search

@@ -20,11 +20,6 @@ export const EMBED_LEFT_BORDER_WIDTH = 4;
export const EMBED_RIGHT_BORDER_WIDTH = 1;
export const EMBED_MEDIA_CHROME_WIDTH = EMBED_PADDING_X * 2 + EMBED_LEFT_BORDER_WIDTH + EMBED_RIGHT_BORDER_WIDTH;
export const EMBED_MEDIA_MAX_WIDTH = 432;
export const EMBED_MEDIA_CONTENT_WIDTH = EMBED_MEDIA_MAX_WIDTH - EMBED_MEDIA_CHROME_WIDTH;
export const EMBED_MEDIA_FILL_CONSTRAINTS = {
maxWidth: EMBED_MEDIA_CONTENT_WIDTH,
maxHeight: EMBED_MEDIA_CONTENT_WIDTH,
} as const;
export interface EmbedProps {
embed: MessageEmbed;
@@ -88,11 +83,6 @@ export const SAVES_DESCRIPTOR = msg({
message: 'saves',
comment: 'Plural external-post stat label on a social post embed. Lowercase to appear after the count.',
});
const thumbnailCalculator = createCalculator({
maxWidth: THUMBNAIL_SIZE,
maxHeight: THUMBNAIL_SIZE,
forceScale: true,
});
const URL_CACHE_CAPACITY = 4096;
const normalizedUrlCache = new Map<string, string | null>();
const hostnameCache = new Map<string, string | null>();
@@ -138,19 +128,6 @@ export const calculateMediaDimensions = (media: Required<EmbedMedia>): MediaDime
const {dimensions} = mediaCalculator.calculate({width: media.width, height: media.height});
return dimensions;
};
export const calculateEmbedImageDimensions = (media: Required<EmbedMedia>): MediaDimensions => {
const naturalWidth = media.width > 0 ? media.width : 1;
const naturalHeight = media.height > 0 ? media.height : 1;
const scale = Math.min(
1,
EMBED_MEDIA_FILL_CONSTRAINTS.maxWidth / naturalWidth,
EMBED_MEDIA_FILL_CONSTRAINTS.maxHeight / naturalHeight,
);
return {
width: Math.max(1, Math.round(naturalWidth * scale)),
height: Math.max(1, Math.round(naturalHeight * scale)),
};
};
export const getOptimizedMediaURL = (proxyURL: string, width: number, height: number, contentType?: string): string => {
const targetWidth = Math.round(width * 2);
const targetHeight = Math.round(height * 2);
@@ -199,13 +176,6 @@ export const mediaPropsEqual = <
if (prev.isPreview !== next.isPreview) return false;
return embedMediaSignature(prev.embed) === embedMediaSignature(next.embed);
};
export const shouldRenderAsInlineThumbnail = (media?: EmbedMedia): boolean => {
if (!isValidMedia(media)) return false;
const {dimensions: thumbnailDimensions} = thumbnailCalculator.calculate({width: media.width, height: media.height});
const thumbnailWidth = thumbnailDimensions.width;
const {width: fullWidth} = calculateEmbedImageDimensions(media);
return fullWidth < 300 && thumbnailWidth >= 40;
};
export const isMediaMatureContent = (media?: EmbedMedia): boolean => {
if (!media) return false;
return Boolean(media.nsfw || ((media.flags ?? 0) & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0);
@@ -8,6 +8,7 @@ import {
isMediaMatureContent,
isValidMedia,
mediaPropsEqual,
THUMBNAIL_SIZE,
} from '@app/features/channel/components/embeds/channel_embed/ChannelEmbedShared';
import {EmbedGif} from '@app/features/channel/components/embeds/media/EmbedGifv';
import {EmbedImage} from '@app/features/channel/components/embeds/media/EmbedImage';
@@ -203,7 +204,7 @@ const InlineThumbnailRendererInner: FC<EmbedMediaRendererProps> = observer(
({embed, message, embedIndex, onDelete, isPreview}) => {
if (!embed.thumbnail || !isValidMedia(embed.thumbnail)) return null;
const thumbnail = embed.thumbnail;
const width = Math.min(80, Math.round((80 * thumbnail.width) / thumbnail.height));
const width = Math.min(THUMBNAIL_SIZE, Math.round((THUMBNAIL_SIZE * thumbnail.width) / thumbnail.height));
const thumbnailIsAnimated =
thumbnail.content_type === 'image/gif' ||
(thumbnail.flags & MessageAttachmentFlags.IS_ANIMATED) === MessageAttachmentFlags.IS_ANIMATED;
@@ -216,14 +217,14 @@ const InlineThumbnailRendererInner: FC<EmbedMediaRendererProps> = observer(
<EmbedImage
src={
thumbnail.content_type === 'image/gif'
? buildAnimatedImageProxyURL(thumbnail.proxy_url, width * 2, 160)
: getOptimizedMediaURL(thumbnail.proxy_url, width, 80, thumbnail.content_type)
? buildAnimatedImageProxyURL(thumbnail.proxy_url, width * 2, THUMBNAIL_SIZE * 2)
: getOptimizedMediaURL(thumbnail.proxy_url, width, THUMBNAIL_SIZE, thumbnail.content_type)
}
originalSrc={thumbnail.url}
naturalWidth={thumbnail.width}
naturalHeight={thumbnail.height}
width={width}
height={80}
height={THUMBNAIL_SIZE}
placeholder={thumbnail.placeholder}
constrain={true}
isInline={true}
@@ -10,7 +10,6 @@ import {
getBorderColor,
getUrlHostname,
isValidMedia,
shouldRenderAsInlineThumbnail,
} from '@app/features/channel/components/embeds/channel_embed/ChannelEmbedShared';
import {
EmbedMediaRenderer,
@@ -48,9 +47,8 @@ export const RichEmbed: FC<EmbedProps> = observer(
[galleryImages, embed, embedIndex, showGallery],
);
const shouldRenderMedia = hasAnyMedia || showGallery;
const isRichType = embed.type === MessageEmbedTypes.RICH;
const isInlineThumbnail =
!hasVideo && hasThumbnail && !hasImage && (isRichType || shouldRenderAsInlineThumbnail(embed.thumbnail));
const promotesThumbnailToImage = embed.type === MessageEmbedTypes.ARTICLE || embed.type === MessageEmbedTypes.IMAGE;
const isInlineThumbnail = !hasVideo && hasThumbnail && !hasImage && !promotesThumbnailToImage;
const shouldRenderInlineThumbnail = isInlineThumbnail && !showGallery;
const isYouTubeEmbed = getUrlHostname(embed.provider?.url) === 'www.youtube.com';
const useNarrowWidth = shouldRenderMedia && !shouldRenderInlineThumbnail;