Fix jumbo emoji sizing being overridden by folds Text inherit.

Render emoji-only messages without folds Text, force 100px on the container and emoticon elements, and pass jumbo through scaleSystemEmoji for plain-text bodies.
This commit is contained in:
2026-07-21 16:21:35 +10:00
parent b912a53548
commit 5c0c5e3758
8 changed files with 120 additions and 30 deletions

View File

@@ -100,7 +100,7 @@ function RenderEmoticonElement({
> >
{element.key.startsWith('mxc://') ? ( {element.key.startsWith('mxc://') ? (
<img <img
className={css.EmoticonImg} className={css.EmoticonImg()}
src={mxcUrlToHttp(mx, element.key, useAuthentication) ?? element.key} src={mxcUrlToHttp(mx, element.key, useAuthentication) ?? element.key}
alt={element.shortcode} alt={element.shortcode}
/> />

View File

@@ -69,6 +69,7 @@ export function BrokenContent() {
type RenderBodyProps = { type RenderBodyProps = {
body: string; body: string;
customBody?: string; customBody?: string;
jumbo?: boolean;
}; };
type MTextProps = { type MTextProps = {
edited?: boolean; edited?: boolean;
@@ -87,17 +88,19 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }:
: undefined; : undefined;
const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG); const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined; const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
const jumbo = isJumboEmoji(trimmedBody, trimmedCustomBody);
return ( return (
<> <>
<MessageTextBody <MessageTextBody
preWrap={typeof customBody !== 'string'} preWrap={typeof customBody !== 'string'}
jumboEmoji={isJumboEmoji(trimmedBody, trimmedCustomBody)} jumboEmoji={jumbo}
style={style} style={style}
> >
{renderBody({ {renderBody({
body: trimmedBody, body: trimmedBody,
customBody: trimmedCustomBody, customBody: trimmedCustomBody,
jumbo,
})} })}
{edited && <MessageEditedContent />} {edited && <MessageEditedContent />}
</MessageTextBody> </MessageTextBody>
@@ -165,17 +168,19 @@ export function MNotice({ edited, content, renderBody, renderUrlsPreview }: MNot
: undefined; : undefined;
const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG); const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined; const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
const jumbo = isJumboEmoji(trimmedBody, trimmedCustomBody);
return ( return (
<> <>
<MessageTextBody <MessageTextBody
notice notice
preWrap={typeof customBody !== 'string'} preWrap={typeof customBody !== 'string'}
jumboEmoji={isJumboEmoji(trimmedBody, trimmedCustomBody)} jumboEmoji={jumbo}
> >
{renderBody({ {renderBody({
body: trimmedBody, body: trimmedBody,
customBody: trimmedCustomBody, customBody: trimmedCustomBody,
jumbo,
})} })}
{edited && <MessageEditedContent />} {edited && <MessageEditedContent />}
</MessageTextBody> </MessageTextBody>

View File

@@ -9,6 +9,7 @@ import { highlightText, scaleSystemEmoji } from '../../plugins/react-custom-html
type RenderBodyProps = { type RenderBodyProps = {
body: string; body: string;
customBody?: string; customBody?: string;
jumbo?: boolean;
highlightRegex?: RegExp; highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions; htmlReactParserOptions: HTMLReactParserOptions;
@@ -17,6 +18,7 @@ type RenderBodyProps = {
export function RenderBody({ export function RenderBody({
body, body,
customBody, customBody,
jumbo = false,
highlightRegex, highlightRegex,
htmlReactParserOptions, htmlReactParserOptions,
linkifyOpts, linkifyOpts,
@@ -29,8 +31,8 @@ export function RenderBody({
return ( return (
<Linkify options={linkifyOpts}> <Linkify options={linkifyOpts}>
{highlightRegex {highlightRegex
? highlightText(highlightRegex, scaleSystemEmoji(body)) ? highlightText(highlightRegex, scaleSystemEmoji(body, jumbo))
: scaleSystemEmoji(body)} : scaleSystemEmoji(body, jumbo)}
</Linkify> </Linkify>
); );
} }

View File

@@ -29,16 +29,35 @@ export const UsernameBold = as<'b'>(({ as: AsUsernameBold = 'b', className, ...p
)); ));
export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?: boolean }>( export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?: boolean }>(
({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => ( ({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => {
<Text const bodyClass = classNames(
as={asComp} css.MessageTextBody({ preWrap, jumboEmoji, emote }),
size={jumboEmoji ? 'Inherit' : 'T400'} className
priority={notice ? '300' : '400'} );
className={classNames(css.MessageTextBody({ preWrap, jumboEmoji, emote }), className)}
data-allow-text-selection="true" // Skip folds Text for jumbo — its DefaultReset sets font-size:inherit and wins the cascade.
data-jumbo-emoji={jumboEmoji ? 'true' : undefined} if (jumboEmoji) {
{...props} return (
ref={ref} <AsComp
/> className={bodyClass}
) data-allow-text-selection="true"
data-jumbo-emoji="true"
{...props}
ref={ref}
/>
);
}
return (
<Text
as={asComp}
size="T400"
priority={notice ? '300' : '400'}
className={bodyClass}
data-allow-text-selection="true"
{...props}
ref={ref}
/>
);
}
); );

View File

@@ -248,15 +248,26 @@ export const MessageTextBody = recipe({
export type MessageTextBodyVariants = RecipeVariants<typeof MessageTextBody>; export type MessageTextBodyVariants = RecipeVariants<typeof MessageTextBody>;
// Prefer data-attr so sizing wins even if recipe class order fights folds Text sizes. /** Jumbo emoji-only messages: 100×100px (system unicode + custom mx-emoticons). */
const jumboEmojiSelector = '[data-jumbo-emoji="true"]'; const jumboEmojiSelector = '[data-jumbo-emoji="true"]';
globalStyle(jumboEmojiSelector, {
fontSize: `${toRem(100)} !important`,
lineHeight: `${toRem(100)} !important`,
});
globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonBase}`, { globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonBase}`, {
height: `${toRem(100)} !important`, height: `${toRem(100)} !important`,
padding: '0 !important', padding: '0 !important',
verticalAlign: 'bottom', verticalAlign: 'bottom',
}); });
globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonBaseJumbo}`, {
height: `${toRem(100)} !important`,
padding: '0 !important',
verticalAlign: 'bottom',
});
globalStyle(`${jumboEmojiSelector} .${htmlCss.Emoticon.classNames.base}`, { globalStyle(`${jumboEmojiSelector} .${htmlCss.Emoticon.classNames.base}`, {
fontSize: `${toRem(100)} !important`, fontSize: `${toRem(100)} !important`,
height: `${toRem(100)} !important`, height: `${toRem(100)} !important`,
@@ -265,7 +276,23 @@ globalStyle(`${jumboEmojiSelector} .${htmlCss.Emoticon.classNames.base}`, {
top: '0 !important', top: '0 !important',
}); });
globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonImg}`, { globalStyle(`${jumboEmojiSelector} .${htmlCss.Emoticon.classNames.variants.jumbo.true}`, {
fontSize: `${toRem(100)} !important`,
height: `${toRem(100)} !important`,
minWidth: `${toRem(100)} !important`,
lineHeight: `${toRem(100)} !important`,
top: '0 !important',
});
globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonImg.classNames.base}`, {
height: `${toRem(100)} !important`,
width: `${toRem(100)} !important`,
maxHeight: `${toRem(100)} !important`,
maxWidth: `${toRem(100)} !important`,
objectFit: 'contain',
});
globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonImg.classNames.variants.jumbo.true}`, {
height: `${toRem(100)} !important`, height: `${toRem(100)} !important`,
width: `${toRem(100)} !important`, width: `${toRem(100)} !important`,
maxHeight: `${toRem(100)} !important`, maxHeight: `${toRem(100)} !important`,

View File

@@ -184,13 +184,19 @@ export const factoryRenderLinkifyWithMention = (
return render; return render;
}; };
export const scaleSystemEmoji = (text: string): (string | JSX.Element)[] => export const scaleSystemEmoji = (text: string, jumbo = false): (string | JSX.Element)[] =>
findAndReplace( findAndReplace(
text, text,
EMOJI_REG_G, EMOJI_REG_G,
(match, pushIndex) => ( (match, pushIndex) => (
<span key={`scaleSystemEmoji-${pushIndex}`} className={css.EmoticonBase}> <span
<span className={css.Emoticon()} title={getShortcodeFor(getHexcodeForEmoji(match[0]))}> key={`scaleSystemEmoji-${pushIndex}`}
className={jumbo ? css.EmoticonBaseJumbo : css.EmoticonBase}
>
<span
className={css.Emoticon({ jumbo })}
title={getShortcodeFor(getHexcodeForEmoji(match[0]))}
>
{match[0]} {match[0]}
</span> </span>
</span> </span>
@@ -557,7 +563,7 @@ export const getReactCustomHtmlParser = (
return ( return (
<span className={css.EmoticonBase}> <span className={css.EmoticonBase}>
<span className={css.Emoticon()}> <span className={css.Emoticon()}>
<AuthenticatedImg {...imgProps} className={css.EmoticonImg} src={htmlSrc} /> <AuthenticatedImg {...imgProps} className={css.EmoticonImg()} src={htmlSrc} />
</span> </span>
</span> </span>
); );

View File

@@ -261,6 +261,15 @@ export const EmoticonBase = style([
}, },
]); ]);
export const EmoticonBaseJumbo = style([
EmoticonBase,
{
height: toRem(100),
padding: 0,
verticalAlign: 'bottom',
},
]);
export const Emoticon = recipe({ export const Emoticon = recipe({
base: [ base: [
DefaultReset, DefaultReset,
@@ -285,16 +294,38 @@ export const Emoticon = recipe({
boxShadow: `0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.OnContainer}`, boxShadow: `0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.OnContainer}`,
}, },
}, },
jumbo: {
true: {
fontSize: toRem(100),
height: toRem(100),
minWidth: toRem(100),
lineHeight: toRem(100),
top: 0,
},
},
}, },
}); });
export const EmoticonImg = style([ export const EmoticonImg = recipe({
DefaultReset, base: [
{ DefaultReset,
height: '1em', {
cursor: 'default', height: '1em',
cursor: 'default',
},
],
variants: {
jumbo: {
true: {
height: toRem(100),
width: toRem(100),
maxHeight: toRem(100),
maxWidth: toRem(100),
objectFit: 'contain',
},
},
}, },
]); });
export const highlightText = style([ export const highlightText = style([
DefaultReset, DefaultReset,

View File

@@ -26,7 +26,7 @@ export const JUMBO_EMOJI_REG = new RegExp(
); );
/** Zero-width / format chars that break naive emoji-only matching. */ /** Zero-width / format chars that break naive emoji-only matching. */
const JUMBO_IGNORE_CHARS_REG = /[\u200B-\u200D\uFEFF\u00AD\u2060]/g; const JUMBO_IGNORE_CHARS_REG = /[\u200B-\u200D\uFEFF\u00AD\u2060\u00A0]/g;
const ONLY_MX_EMOTICON_HTML_REG = const ONLY_MX_EMOTICON_HTML_REG =
/^(?:\s|<br\s*\/?>)*((?:<img\b[^>]*\bdata-mx-emoticon\b[^>]*\/?>)(?:\s|<br\s*\/?>)*){1,10}$/i; /^(?:\s|<br\s*\/?>)*((?:<img\b[^>]*\bdata-mx-emoticon\b[^>]*\/?>)(?:\s|<br\s*\/?>)*){1,10}$/i;