diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx index f86323e..5459c48 100644 --- a/src/app/components/message/MsgTypeRenderers.tsx +++ b/src/app/components/message/MsgTypeRenderers.tsx @@ -2,7 +2,7 @@ import React, { CSSProperties, ReactNode } from 'react'; import { Box, Chip, Text, toRem } from 'folds'; import { Icon, Icons } from '../icons'; 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 { MessageTextBody } from './layout'; import { @@ -92,7 +92,7 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }: <> {renderBody({ @@ -135,7 +135,7 @@ export function MEmote({ {`${displayName} `} {renderBody({ @@ -171,7 +171,7 @@ export function MNotice({ edited, content, renderBody, renderUrlsPreview }: MNot {renderBody({ body: trimmedBody, diff --git a/src/app/components/message/layout/Base.tsx b/src/app/components/message/layout/Base.tsx index aa5ffbf..549b6f4 100644 --- a/src/app/components/message/layout/Base.tsx +++ b/src/app/components/message/layout/Base.tsx @@ -32,10 +32,11 @@ export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice? ({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => ( diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts index 64d71fa..c1ff024 100644 --- a/src/app/components/message/layout/layout.css.ts +++ b/src/app/components/message/layout/layout.css.ts @@ -232,6 +232,9 @@ export const MessageTextBody = recipe({ true: { fontSize: toRem(100), lineHeight: toRem(100), + // Don't clip oversized glyphs / custom emotes in jumbo mode + overflow: 'visible', + overflowY: 'visible', }, }, emote: { @@ -245,24 +248,27 @@ export const MessageTextBody = recipe({ export type MessageTextBodyVariants = RecipeVariants; -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}`, { - height: toRem(100), - padding: 0, +globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonBase}`, { + height: `${toRem(100)} !important`, + padding: '0 !important', verticalAlign: 'bottom', }); -globalStyle(`${jumboEmojiClass} .${htmlCss.Emoticon.classNames.base}`, { - fontSize: toRem(100), - height: toRem(100), - minWidth: toRem(100), - lineHeight: toRem(100), - top: 0, +globalStyle(`${jumboEmojiSelector} .${htmlCss.Emoticon.classNames.base}`, { + fontSize: `${toRem(100)} !important`, + height: `${toRem(100)} !important`, + minWidth: `${toRem(100)} !important`, + lineHeight: `${toRem(100)} !important`, + top: '0 !important', }); -globalStyle(`${jumboEmojiClass} .${htmlCss.EmoticonImg}`, { - height: toRem(100), - width: toRem(100), +globalStyle(`${jumboEmojiSelector} .${htmlCss.EmoticonImg}`, { + height: `${toRem(100)} !important`, + width: `${toRem(100)} !important`, + maxHeight: `${toRem(100)} !important`, + maxWidth: `${toRem(100)} !important`, objectFit: 'contain', }); diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index da484bd..96aa9f8 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -552,10 +552,12 @@ export const getReactCustomHtmlParser = ( ); } 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 ( - + ); diff --git a/src/app/utils/regex.ts b/src/app/utils/regex.ts index 0b98b0e..5a82a46 100644 --- a/src/app/utils/regex.ts +++ b/src/app/utils/regex.ts @@ -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( `^(((${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|)*((?:]*\bdata-mx-emoticon\b[^>]*\/?>)(?:\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(/(\s*)+$/gi, ''); + if (html.length > 0 && ONLY_MX_EMOTICON_HTML_REG.test(html)) return true; + } + + return false; +};