Fix standalone emoji not rendering at jumbo 100px size.

Detect emoji-only messages more reliably (including mx-emoticon HTML), bypass folds Text sizing when jumbo, and force 100px emote dimensions via data-jumbo-emoji styles.
This commit is contained in:
2026-07-21 15:56:02 +10:00
parent 9f8e446146
commit b912a53548
5 changed files with 53 additions and 19 deletions

View File

@@ -2,7 +2,7 @@ import React, { CSSProperties, ReactNode } from 'react';
import { Box, Chip, Text, toRem } from 'folds'; import { Box, Chip, Text, toRem } from 'folds';
import { Icon, Icons } from '../icons'; import { Icon, Icons } from '../icons';
import { IContent } from 'matrix-js-sdk'; import { IContent } from 'matrix-js-sdk';
import { JUMBO_EMOJI_REG, URL_REG } from '../../utils/regex'; import { URL_REG, isJumboEmoji } from '../../utils/regex';
import { trimReplyFromBody } from '../../utils/room'; import { trimReplyFromBody } from '../../utils/room';
import { MessageTextBody } from './layout'; import { MessageTextBody } from './layout';
import { import {
@@ -92,7 +92,7 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }:
<> <>
<MessageTextBody <MessageTextBody
preWrap={typeof customBody !== 'string'} preWrap={typeof customBody !== 'string'}
jumboEmoji={JUMBO_EMOJI_REG.test(trimmedBody)} jumboEmoji={isJumboEmoji(trimmedBody, trimmedCustomBody)}
style={style} style={style}
> >
{renderBody({ {renderBody({
@@ -135,7 +135,7 @@ export function MEmote({
<MessageTextBody <MessageTextBody
emote emote
preWrap={typeof customBody !== 'string'} preWrap={typeof customBody !== 'string'}
jumboEmoji={JUMBO_EMOJI_REG.test(trimmedBody)} jumboEmoji={false}
> >
<b>{`${displayName} `}</b> <b>{`${displayName} `}</b>
{renderBody({ {renderBody({
@@ -171,7 +171,7 @@ export function MNotice({ edited, content, renderBody, renderUrlsPreview }: MNot
<MessageTextBody <MessageTextBody
notice notice
preWrap={typeof customBody !== 'string'} preWrap={typeof customBody !== 'string'}
jumboEmoji={JUMBO_EMOJI_REG.test(trimmedBody)} jumboEmoji={isJumboEmoji(trimmedBody, trimmedCustomBody)}
> >
{renderBody({ {renderBody({
body: trimmedBody, body: trimmedBody,

View File

@@ -32,10 +32,11 @@ export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?
({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => ( ({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => (
<Text <Text
as={asComp} as={asComp}
size="T400" size={jumboEmoji ? 'Inherit' : 'T400'}
priority={notice ? '300' : '400'} priority={notice ? '300' : '400'}
className={classNames(css.MessageTextBody({ preWrap, jumboEmoji, emote }), className)} className={classNames(css.MessageTextBody({ preWrap, jumboEmoji, emote }), className)}
data-allow-text-selection="true" data-allow-text-selection="true"
data-jumbo-emoji={jumboEmoji ? 'true' : undefined}
{...props} {...props}
ref={ref} ref={ref}
/> />

View File

@@ -232,6 +232,9 @@ export const MessageTextBody = recipe({
true: { true: {
fontSize: toRem(100), fontSize: toRem(100),
lineHeight: toRem(100), lineHeight: toRem(100),
// Don't clip oversized glyphs / custom emotes in jumbo mode
overflow: 'visible',
overflowY: 'visible',
}, },
}, },
emote: { emote: {
@@ -245,24 +248,27 @@ export const MessageTextBody = recipe({
export type MessageTextBodyVariants = RecipeVariants<typeof MessageTextBody>; export type MessageTextBodyVariants = RecipeVariants<typeof MessageTextBody>;
const jumboEmojiClass = MessageTextBody.classNames.variants.jumboEmoji.true; // Prefer data-attr so sizing wins even if recipe class order fights folds Text sizes.
const jumboEmojiSelector = '[data-jumbo-emoji="true"]';
globalStyle(`${jumboEmojiClass} .${htmlCss.EmoticonBase}`, { globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonBase}`, {
height: toRem(100), height: `${toRem(100)} !important`,
padding: 0, padding: '0 !important',
verticalAlign: 'bottom', verticalAlign: 'bottom',
}); });
globalStyle(`${jumboEmojiClass} .${htmlCss.Emoticon.classNames.base}`, { globalStyle(`${jumboEmojiSelector} .${htmlCss.Emoticon.classNames.base}`, {
fontSize: toRem(100), fontSize: `${toRem(100)} !important`,
height: toRem(100), height: `${toRem(100)} !important`,
minWidth: toRem(100), minWidth: `${toRem(100)} !important`,
lineHeight: toRem(100), lineHeight: `${toRem(100)} !important`,
top: 0, top: '0 !important',
}); });
globalStyle(`${jumboEmojiClass} .${htmlCss.EmoticonImg}`, { globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonImg}`, {
height: toRem(100), height: `${toRem(100)} !important`,
width: toRem(100), width: `${toRem(100)} !important`,
maxHeight: `${toRem(100)} !important`,
maxWidth: `${toRem(100)} !important`,
objectFit: 'contain', objectFit: 'contain',
}); });

View File

@@ -552,10 +552,12 @@ export const getReactCustomHtmlParser = (
); );
} }
if (htmlSrc && 'data-mx-emoticon' in props) { if (htmlSrc && 'data-mx-emoticon' in props) {
// Drop presentational height/width so jumbo CSS can size freely
const { height: _h, width: _w, ...imgProps } = props;
return ( return (
<span className={css.EmoticonBase}> <span className={css.EmoticonBase}>
<span className={css.Emoticon()}> <span className={css.Emoticon()}>
<AuthenticatedImg {...props} className={css.EmoticonImg} src={htmlSrc} /> <AuthenticatedImg {...imgProps} className={css.EmoticonImg} src={htmlSrc} />
</span> </span>
</span> </span>
); );

View File

@@ -24,3 +24,28 @@ export const EMOJI_PATTERN = `[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u
export const JUMBO_EMOJI_REG = new RegExp( export const JUMBO_EMOJI_REG = new RegExp(
`^(((${EMOJI_PATTERN})|(:.+?:))(${VARIATION_SELECTOR_PATTERN}|\\s)*){1,10}$` `^(((${EMOJI_PATTERN})|(:.+?:))(${VARIATION_SELECTOR_PATTERN}|\\s)*){1,10}$`
); );
/** Zero-width / format chars that break naive emoji-only matching. */
const JUMBO_IGNORE_CHARS_REG = /[\u200B-\u200D\uFEFF\u00AD\u2060]/g;
const ONLY_MX_EMOTICON_HTML_REG =
/^(?:\s|<br\s*\/?>)*((?:<img\b[^>]*\bdata-mx-emoticon\b[^>]*\/?>)(?:\s|<br\s*\/?>)*){1,10}$/i;
/**
* True when a message is emoji/custom-emote only (Discord-style jumbo).
* Strips zero-width chars and also accepts HTML that is only mx-emoticon imgs.
*/
export const isJumboEmoji = (body: string, customBody?: string): boolean => {
const plain = body.replace(JUMBO_IGNORE_CHARS_REG, '').trim();
if (plain.length > 0 && JUMBO_EMOJI_REG.test(plain)) return true;
if (typeof customBody === 'string') {
const html = customBody
.replace(JUMBO_IGNORE_CHARS_REG, '')
.trim()
.replace(/(<br\s*\/?>\s*)+$/gi, '');
if (html.length > 0 && ONLY_MX_EMOTICON_HTML_REG.test(html)) return true;
}
return false;
};